From d3820f6d2eb853c2e933da2163e7d13aafb616a0 Mon Sep 17 00:00:00 2001 From: Doyub Kim Date: Wed, 12 Aug 2026 12:37:04 -0700 Subject: [PATCH] Publish CAD-to-SimReady 0.2.0 Signed-off-by: Doyub Kim --- skills/omniverse-cad-to-simready/BENCHMARK.md | 146 +- skills/omniverse-cad-to-simready/CHANGELOG.md | 46 + skills/omniverse-cad-to-simready/SKILL.md | 209 +-- .../config/skillspector-baseline.yaml | 31 + .../evals/evals.json | 172 +- .../assemble-package-source/README.md | 24 +- .../scripts/check_dependencies.py | 37 + .../assemble-package-source/scripts/run.py | 29 +- .../references/commands.md | 2 +- .../references/content-agents/README.md | 9 +- .../material-agent-client/README.md | 21 +- .../references/physics-agent-client/README.md | 12 +- .../references/texture-agent-client/README.md | 8 +- .../scripts/content_agent_client.py | 143 +- .../references/convert-to-usd/README.md | 63 +- .../references/mujoco-usd-converter/README.md | 2 +- .../openusd-exchange-sdk-usdex/README.md | 14 +- .../references/urdf-usd-converter/README.md | 2 +- .../references/usd-convert-cad/README.md | 158 +- .../scripts/check_dependencies.py | 198 +-- .../scripts/kit_app_template_cad.py | 775 -------- .../references/usd-convert-cad/scripts/run.py | 692 ++++---- .../references/usd-convert-gsplat/README.md | 6 +- .../usd-convert-gsplat/scripts/run.py | 6 +- .../deploy-content-agents/README.md | 81 +- .../deploy-material-agent-docker.md | 4 +- .../references/deploy-ovrtx-docker.md | 4 +- .../references/deploy-physics-agent-docker.md | 4 +- .../references/deploy-texture-agent-docker.md | 4 +- .../README.md | 4 +- .../nv-core-package-sample/README.md | 6 +- .../scripts/check_dependencies.py | 8 + .../scripts/check_dependencies.py | 8 + .../scripts/check_dependencies.py | 11 + .../omni-asset-validate/scripts/run.py | 20 + .../references/ovrtx-render-service/README.md | 19 +- .../scripts/check_dependencies.py | 13 + .../ovrtx-render-service/scripts/run.py | 30 +- .../scripts/stage_prep.py | 87 +- .../ovrtx-render-service/scripts/turntable.py | 4 +- .../references/preflight/README.md | 177 +- .../references/preflight/scripts/preflight.py | 1581 ++++++++++++++--- .../references/publishing-layout.md | 31 + .../simready-conform-profile/README.md | 42 +- .../references/FET_000_CORE/README.md | 6 +- .../references/FET_000_CORE/scripts/run.py | 2 +- .../references/FET_001_MINIMAL/README.md | 6 +- .../references/FET_001_MINIMAL/scripts/run.py | 2 +- .../README.md | 6 +- .../FET_005_SIMULATE_GRASP_PHYSICS/README.md | 8 +- .../scripts/check_dependencies.py | 9 + .../simready-conform-profile/scripts/run.py | 7 + .../references/simready-validate/README.md | 24 +- .../scripts/check_dependencies.py | 4 +- .../simready-validate/scripts/run.py | 130 +- .../references/troubleshooting.md | 51 + .../scripts/check_dependencies.py | 11 + .../validate-usd-minimum/scripts/run.py | 8 + .../references/workflow.md | 38 +- .../shared/preflight_manifest.py | 112 +- .../shared/script_utils.py | 45 + .../shared/simready_package.py | 14 + .../simready_package_check_dependencies.py | 12 + .../shared/upstream_versions.py | 67 + .../shared/usd_convert_cad_diagnostics.py | 78 - .../omniverse-cad-to-simready/skill-card.md | 65 +- .../omniverse-cad-to-simready/skill.oms.sig | 2 +- .../upstream-versions.lock.json | 81 + 68 files changed, 3432 insertions(+), 2299 deletions(-) create mode 100644 skills/omniverse-cad-to-simready/CHANGELOG.md create mode 100644 skills/omniverse-cad-to-simready/config/skillspector-baseline.yaml delete mode 100644 skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-cad/scripts/kit_app_template_cad.py create mode 100644 skills/omniverse-cad-to-simready/references/publishing-layout.md create mode 100644 skills/omniverse-cad-to-simready/references/troubleshooting.md create mode 100644 skills/omniverse-cad-to-simready/shared/upstream_versions.py delete mode 100644 skills/omniverse-cad-to-simready/shared/usd_convert_cad_diagnostics.py create mode 100644 skills/omniverse-cad-to-simready/upstream-versions.lock.json diff --git a/skills/omniverse-cad-to-simready/BENCHMARK.md b/skills/omniverse-cad-to-simready/BENCHMARK.md index a44e1fd9..4b0d7c2c 100644 --- a/skills/omniverse-cad-to-simready/BENCHMARK.md +++ b/skills/omniverse-cad-to-simready/BENCHMARK.md @@ -1,95 +1,103 @@ -# Evaluation Report +# Skill Benchmark: omniverse-cad-to-simready -Evaluation of the `omniverse-cad-to-simready` skill before publication through NVSkills-Eval. +> ✅ **Overall verdict: PASS — Recommended for publication** -This benchmark summarizes 3-Tier Evaluation from NVSkills-Eval results for the skill. The goal is to document whether the skill is safe, discoverable, effective, and useful for agents before it is published for broader workflow use. +## Publication Recommendation + +Recommended for publication based on the completed evaluation evidence in this report. -## Evaluation Summary +## Evaluation Metadata - Skill: `omniverse-cad-to-simready` -- Evaluation date: 2026-05-28 -- NVSkills-Eval profile: `external` -- Overall verdict: FAIL -- Tier 3 live agent evaluation: not available in this report +- Evaluation date: 2026-08-12 +- Evaluator version: `1.2.4` +- Agents: Claude Code (`aws/anthropic/bedrock-claude-opus-4-8`), Codex (`openai/openai/gpt-5.5`) +- Tasks: 8 evaluation tasks (7 positive, 1 negative) +- Dataset digest: `sha256:ce412af18bb5e516371ec07ea8752e4043a1d00edd007b505e30f659b8d92047` (skill-evaluator-dataset-snapshot/1) +- Attempts per task: 1 +- Environment: `k8s-sandbox` +- Tier 3 evidence: required for publication -## Agents Used +Each task attempt ran in its own isolated sandbox pod. -- Tier 3 agent details were not available in this report. +## What This Report Answers -## Metrics Used +The three-tier evaluation checks whether the skill: -Reported benchmark dimensions: +- is safe to use; +- produces correct answers; +- is discovered and activated when needed; +- helps the agent complete the user's goal and expected workflow; and +- avoids wasted skill and tool usage. -- Security: checks whether skill-assisted execution avoids unsafe behavior such as secret leakage, destructive commands, or unauthorized access. -- Correctness: checks whether the agent follows the expected workflow and produces the correct final output. -- Discoverability: checks whether the agent loads the skill when relevant and avoids using it when irrelevant. -- Effectiveness: checks whether the agent performs measurably better with the skill than without it. -- Efficiency: checks whether the agent uses fewer tokens and avoids redundant work. +## Results at a Glance -Underlying evaluation signals used in this run: +| Measure | Claude Code (Baseline → Skill Uplift) | Codex (Baseline → Skill Uplift) | +|---|---:|---:| +| Overall | Not available | 48% → 82% (+33 points) | +| Security | Not available | 50% → 81% (+31 points) | +| Correctness | Not available | 57% → 88% (+30 points) | +| Discoverability | Not available | 48% → 81% (+34 points) | +| Effectiveness | Not available | 37% → 75% (+38 points) | +| Efficiency | Not available | 49% → 83% (+34 points) | -- No Tier 3 evaluation signal details were available in this report. +**How to read this table:** baseline is the same task attempted without the target skill. Uplift is `skill score - baseline score`, shown in percentage points. -## Test Tasks +Example: `47% → 92% (+45 points)` means the skill-assisted run scored 92%, 45 percentage points above its 47% no-skill baseline. -Tier 3 evaluation task details were not available in this report. +## Tier Status -## Results +| Tier | Purpose | Status | Evidence | +|---|---|---|---| +| Tier 1 | Static validation | **PASSED WITH OBSERVATIONS** | 1 validator(s); 6 finding(s) | +| Tier 2 | Semantic deduplication | **NOT RUN** | No result was recorded | +| Tier 3 | Live agent evaluation | **PASS** | 2 agent(s); 8 task(s) | -Tier 3 dimension rollup was not available in this report. +## Findings and Observations -## Tier 1: Static Validation Summary +
+Show detailed findings and successful checks -Tier 1 validation passed with observations. NVSkills-Eval ran 9 checks and found 10 total findings. +- **MEDIUM** SCHEMA/frontmatter_field_placement: Root field 'version' is ignored; use 'metadata.version' (`skills/omniverse-cad-to-simready/SKILL.md`) +- **MEDIUM** SCHEMA/body_recommended_section: Missing recommended section: '## Examples' (`skills/omniverse-cad-to-simready/SKILL.md`) +- **LOW** SCHEMA/unexpected_file: Unexpected 'upstream-versions.lock.json' in skill root (`skills/omniverse-cad-to-simready/upstream-versions.lock.json`) +- **LOW** SCHEMA/unexpected_file: Unexpected 'shared' in skill root (`skills/omniverse-cad-to-simready/shared`) +- **LOW** SCHEMA/unexpected_file: Unexpected 'CHANGELOG.md' in skill root (`skills/omniverse-cad-to-simready/CHANGELOG.md`) +- 1 additional finding(s) are available in the full evaluation artifacts. -Top findings: +
-- MEDIUM SCHEMA/body_recommended_section: Missing recommended section: '## Examples' (`skills/omniverse-cad-to-simready/SKILL.md`) -- LOW QUALITY/quality_correctness: No examples provided (`skills/omniverse-cad-to-simready/SKILL.md`) -- LOW QUALITY/quality_discoverability: Description very long (422 chars, recommend 50-150) (`skills/omniverse-cad-to-simready/SKILL.md`) -- LOW QUALITY/quality_discoverability: No '## Purpose' section (`skills/omniverse-cad-to-simready/SKILL.md`) -- LOW SCHEMA/unexpected_file: Unexpected 'BENCHMARK.md' in skill root (`skills/omniverse-cad-to-simready/BENCHMARK.md`) +## Scoring Methodology -## Tier 2: Deduplication Summary +
+Show dimension definitions, source signals, and thresholds -Tier 2 validation reported findings. NVSkills-Eval ran 2 checks and found 13 total findings. +| Dimension | Question | Scored signals | +|---|---|---| +| Security | Is it safe to use? | `security` (100%) | +| Correctness | Is the answer correct? | `accuracy` (100%) | +| Discoverability | Was the right skill loaded when needed? | `skill_execution` (100%) | +| Effectiveness | Did the skill help complete the task? | `goal_accuracy` (50%) + `behavior_check` (50%) | +| Efficiency | Did it avoid wasted tool or skill usage? | `skill_efficiency` (100%) | -Top findings: +- Dimension bands: PASS at 50% or above; NEUTRAL from 40% to below 50%; FAIL below 40%. +- Overall Tier 3 lift: PASS at +5 points or more; FAIL at -10 points or less; values between those bands are NEUTRAL. +- Overall verdict: PASS only when every configured dimension passes for at least one supported agent. Lift is reported as diagnostic evidence and does not override this gate. +- The 50% attempt pass threshold is a separate per-task gate; it is not the dimension pass threshold. +- Effectiveness is the equal-weight mean of goal completion (`goal_accuracy`) and expected workflow adherence (`behavior_check`). +- Token efficiency is a separate report-only signal. It does not change a dimension score or the overall verdict. -- HIGH DUPLICATE/duplicate: Duplicate content found across references/omni-asset-validate-geometry/scripts/run.py and references/omni-asset-validate-physics/scripts/run.py: - "validate()" in references/omni-asset-validate-geometry/scripts/run.py (lines 25-35) - vs "validate()" in references/omni-asset-validate-physics/scripts/run.py (lines 25-35) (`references/omni-asset-validate-geometry/scripts/run.py:25`) -- HIGH DUPLICATE/duplicate: Duplicate content found within references/simready-conform-profile/scripts/run.py: - "_run_fet000()" in references/simready-conform-profile/scripts/run.py (lines 164-216) - vs "_run_fet001()" in references/simready-conform-profile/scripts/run.py (lines 219-256) - vs "_run_fet004()" in references/simready-conform-profile/scripts/run.py (lines 259-299) - vs "_run_fet005()" in references/simready-conform-profile/scripts/run.py (lines 302-367) (`references/simready-conform-profile/scripts/run.py:164`) -- HIGH DUPLICATE/duplicate: Duplicate content found across references/content-agents/scripts/content_agent_client.py and references/content-agents/scripts/run.py and references/convert-to-usd/references/mujoco-usd-converter/scripts/run.py and references/convert-to-usd/references/urdf-usd-converter/scripts/run.py and references/convert-to-usd/references/usd-convert-cad/scripts/run.py and references/convert-to-usd/references/usd-convert-gsplat/scripts/run.py and references/identify-asset-context/scripts/run.py and references/omni-asset-validate-geometry/scripts/run.py and references/omni-asset-validate-physics/scripts/run.py and references/ovrtx-render-service/scripts/run.py and references/ovrtx-render-service/scripts/turntable.py and references/simready-conform-profile/references/FET_000_CORE/scripts/run.py and references/simready-conform-profile/references/FET_001_MINIMAL/scripts/run.py and references/simready-conform-profile/references/FET_004_SIMULATE_MULTI_BODY_PHYSICS/scripts/run.py and references/simready-conform-profile/references/FET_005_SIMULATE_GRASP_PHYSICS/scripts/author_grasp_line.py and references/simready-conform-profile/scripts/run.py and references/simready-validate/scripts/run.py and shared/simready_package.py: - "_emit_report()" in references/content-agents/scripts/content_agent_client.py (lines 1276-1277) - vs "emit()" in references/content-agents/scripts/run.py (lines 217-230) - vs "emit_probe()" in references/convert-to-usd/references/mujoco-usd-converter/scripts/run.py (lines 216-217) - vs "emit_probe()" in references/convert-to-usd/references/urdf-usd-converter/scripts/run.py (lines 214-215) - vs "emit_probe()" in references/convert-to-usd/references/usd-convert-cad/scripts/run.py (lines 554-555) - vs "emit_probe()" in references/convert-to-usd/references/usd-convert-gsplat/scripts/run.py (lines 311-312) - vs "_emit()" in references/identify-asset-context/scripts/run.py (lines 275-276) - vs "emit()" in references/omni-asset-validate-geometry/scripts/run.py (lines 38-44) - vs "emit()" in references/omni-asset-validate-physics/scripts/run.py (lines 38-44) - vs "_emit()" in references/ovrtx-render-service/scripts/run.py (lines 358-359) - vs "_emit()" in references/ovrtx-render-service/scripts/turntable.py (lines 227-228) - vs "emit()" in references/simready-conform-profile/references/FET_000_CORE/scripts/run.py (lines 213-219) - vs "emit()" in references/simready-conform-profile/references/FET_001_MINIMAL/scripts/run.py (lines 331-338) - vs "emit()" in references/simready-conform-profile/references/FET_004_SIMULATE_MULTI_BODY_PHYSICS/scripts/run.py (lines 275-288) - vs "write_reports()" in references/simready-conform-profile/references/FET_005_SIMULATE_GRASP_PHYSICS/scripts/author_grasp_line.py (lines 68-96) - vs "emit()" in references/simready-conform-profile/scripts/run.py (lines 464-475) - vs "emit()" in references/simready-validate/scripts/run.py (lines 700-706) - vs "_emit()" in shared/simready_package.py (lines 662-663) (`references/content-agents/scripts/content_agent_client.py:1276`) -- HIGH DUPLICATE/duplicate: Duplicate content found across references/content-agents/README.md and references/content-agents/references/material-agent-client/README.md: - "## Rate Limits" in references/content-agents/README.md (lines 125-133) - vs "## Rate Limits" in references/content-agents/references/material-agent-client/README.md (lines 93-101) (`references/content-agents/README.md:125`) -- HIGH DUPLICATE/duplicate: Duplicate content found across references/nv-core-package-sample-validation/README.md and references/nv-core-package-sample/README.md: - "## Upstream Reference" in references/nv-core-package-sample/README.md (lines 21-32) - vs "## Upstream Reference" in references/nv-core-package-sample-validation/README.md (lines 9-20) (`references/nv-core-package-sample/README.md:21`) +Signals present in this run: -## Publication Recommendation +- `security` (Security): unsafe operations, secret leakage, and unauthorized access. +- `skill_execution` (Skill Execution): whether the expected skill was found and executed. +- `skill_efficiency` (Efficiency): routing quality, workspace-aware skill reads, and productive tool use. +- `accuracy` (Accuracy): final-answer correctness against the reference answer. +- `goal_accuracy` (Goal Accuracy): whether the user's goal was achieved. +- `behavior_check` (Behavior Check): whether the expected workflow behavior was followed. + +
+ +## Freshness -The skill should be reviewed before NVSkills-Eval publication. Skill owners should address the findings above and rerun NVSkills-Eval to refresh this benchmark. +Regenerate this benchmark when the skill, evaluation dataset, target agent/model, evaluator version, environment, or scoring policy changes. diff --git a/skills/omniverse-cad-to-simready/CHANGELOG.md b/skills/omniverse-cad-to-simready/CHANGELOG.md new file mode 100644 index 00000000..72666d43 --- /dev/null +++ b/skills/omniverse-cad-to-simready/CHANGELOG.md @@ -0,0 +1,46 @@ +# CAD-to-SimReady Changelog + +## 0.2.0 (Unreleased) + +This release improves the reliability, portability, dependency management, and +security-scanner compatibility of the `omniverse-cad-to-simready` workflow. + +### Added + +- Configurable Content Agents deployment for NVIDIA, OpenAI-compatible, + Anthropic, and Google/Gemini model providers. +- Deterministic upstream dependency pins and a checked-in tested-version lock + file for conversion, validation, rendering, and Content Agents integrations. +- Expanded preflight reporting, runtime validation, and evaluation coverage. +- An audited SkillSpector baseline using exact fingerprints for reviewed false + positives. New or changed findings continue to fail closed. + +### Changed + +- Migrated CAD conversion to the self-contained `usd-convert-cad` 0.2.0 wheel + in an isolated Python 3.12 environment. +- Simplified the top-level workflow router while preserving detailed + stage-specific guidance in nested references. +- Improved conversion-only routing so Content Agents deployment is skipped when + material and physics assignment are not requested. +- Updated the CAD-to-SimReady skill and skill-card version to 0.2.0. + +### Fixed + +- Hardened credential environment-variable validation, forwarding, redaction, + and generated configuration references. +- Improved reuse and validation of existing Content Agents endpoints, including + local Docker host translation. +- Rejected missing, blank, or uniform render and thumbnail outputs instead of + accepting them as successful inspections. +- Correctly counted geometry represented through USD instance proxies. +- Improved SimReady validation, optimizer recovery, dependency checks, and + actionable error reporting. + +### Migration Notes + +- Use Python 3.12 for the workflow and its managed environments. +- Install and invoke `usd-convert-cad` 0.2.0 as a wheel. The previous KAT/Kit + fallback is no longer supported. +- Treat `upstream-versions.lock.json` as the tested dependency source of truth; + preflight blocks on incompatible direct or transitive package pins. diff --git a/skills/omniverse-cad-to-simready/SKILL.md b/skills/omniverse-cad-to-simready/SKILL.md index f8269c99..0ef92ad0 100644 --- a/skills/omniverse-cad-to-simready/SKILL.md +++ b/skills/omniverse-cad-to-simready/SKILL.md @@ -1,17 +1,20 @@ --- name: omniverse-cad-to-simready description: "Coordinate the end-to-end CAD/source-asset to SimReady workflow. Use for broad requests such as CAD to SimReady, source asset to simulation-ready USD, or prop packaging that require conversion, material/physics assignment, SimReady conformance, validation, and optional package creation; deploy or verify Content Agents services first when property assignment is enabled; route single-stage work through nested references." -version: "0.1.0" +version: "0.2.0" license: Apache-2.0 tools: - Read - Shell compatibility: > - Orchestrator skill. Managed Content Agents deployment requires NVIDIA_API_KEY - (build.nvidia.com), Docker + NVIDIA Container Toolkit + GPU, Python 3.12, and - an upstream checkout of nvidia-omniverse/content-agents on branch - main. Reused/provided endpoints may instead use explicit endpoint and - usage-token environment variables. Linux/macOS only. + Orchestrator skill. Managed Content Agents deployment requires a configured + model provider key matching the selected backend, such as NVIDIA_API_KEY, + OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, or GEMINI_API_KEY, + Docker + NVIDIA Container Toolkit + GPU, Python 3.12, and an upstream checkout of + nvidia-omniverse/content-agents at the ref pinned in upstream-versions.lock.json. + Reused/provided endpoints may + instead use explicit endpoint and usage-token environment variables. + Linux/macOS only. metadata: author: Omniverse tags: @@ -29,85 +32,55 @@ metadata: ## When to Use -Use this workflow skill when the user wants an end-to-end pipeline from a -source asset to a SimReady asset or package. This skill coordinates existing -conversion, authoring, validation, conformance, rendering, and packaging -references directly. Do not replace the workflow with a single monolithic -runner command. +Use this workflow skill for an end-to-end pipeline from a source asset to a +SimReady asset or package. It coordinates existing conversion, authoring, +validation, conformance, rendering, and packaging references directly; do not +replace it with a single monolithic runner command. -This skill is documentation-driven and does not ship `scripts/run.py`. It -should not depend on a repository checkout. When a stage needs deterministic -execution, run the portable script from that stage reference's installed -directory. `Shell` is declared because this workflow invokes installed stage -reference scripts directly; it still must not grow a monolithic runner. +This skill is documentation-driven and does not ship `scripts/run.py`; it must +not depend on a repository checkout. `Shell` is declared because this workflow +invokes installed stage reference scripts directly, from each reference's +installed directory, and it still must not grow a monolithic runner. ## Prerequisites -- Prefer running the `preflight` reference first for deterministic setup. It - installs or verifies local upstream checkouts, writes a - `cad-to-simready-preflight.json` manifest, and exports - `PHYSICAL_AI_PREFLIGHT_MANIFEST` plus `PHYSICAL_AI_REQUIRE_PREFLIGHT=1` for - downstream references. +- Prefer `preflight` first for deterministic setup: it installs/verifies + local upstream checkouts, writes a `cad-to-simready-preflight.json` + manifest, and exports `PHYSICAL_AI_PREFLIGHT_MANIFEST` plus + `PHYSICAL_AI_REQUIRE_PREFLIGHT=1` for downstream references. - Python 3.12 and `uv` (per repo `README.md`). -- NVIDIA_API_KEY from `https://build.nvidia.com` when local Content Agents - deployment will run. Already-running endpoints may instead use explicit - endpoint variables plus usage tokens such as `NGC_API_KEY`, `NVCF_API_KEY`, - or `CONTENT_AGENTS_*_TOKEN`. +- A Content Agents model provider key when local deployment will run, or + explicit endpoint variables plus usage tokens for already-running + endpoints; see `references/preflight/README.md` for the full list. - Docker, NVIDIA Container Toolkit, and an NVIDIA GPU for Content Agents and OVRTX stages. - Local upstream checkouts under - `${PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT:-$HOME/.physical-ai-skill-hub/upstreams}` - when a downstream stage needs upstream scripts or specs. - -## Minimum Viable Scope - -Conversion-only is a valid workflow request. When the user asks only to convert -or smoke-test source asset conversion, set `property_assignment_intent=skip`, -do not deploy Content Agents, run `convert-to-usd`, then run -`validate-usd-minimum` on the generated USD if conversion succeeds. - -Do not imply that `uv sync` installs every source converter runtime. URDF, -MuJoCo/MJCF, and the repo Python dependencies are handled by the project -environment, but NVIDIA-backed source conversion requires an installed and -validated `NVIDIA-Omniverse/usd-convert-cad` checkout. If that runtime is -missing or does not support the source, preserve the blocked conversion report -and its `install_hint` instead of attempting an unrequested local build or -substituting another converter. + `${OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT:-$HOME/.omniverse-cad-to-simready/upstreams}` + when a stage needs upstream scripts or specs. ## First Action For any broad CAD/source-asset to SimReady request, assume `property_assignment_intent=run` unless the user explicitly asks for conversion-only, validation-only, or no material/physics assignment. - -Before invoking converter, validation, Content Agents, OVRTX, packaging, or FET -helper scripts, run the `preflight` reference or verify an existing -`PHYSICAL_AI_PREFLIGHT_MANIFEST`. Treat preflight as the mandatory dependency -bootstrap step, not as workflow routing. If the user explicitly asks not to -deploy services or asks for conversion-only/validation-only, use -`--skip-content-agents`. -When `PHYSICAL_AI_REQUIRE_PREFLIGHT=1` is set and a required component is not -ready in the manifest, downstream references must block with the preflight -guardrail instead of rediscovering upstreams or services directly. - -When `property_assignment_intent=run`, the first operational action after -confirming the source path and resolving intent is to verify or deploy Content -Agents services. Do this before asset-context inspection, converter dependency -checks, conversion, validation, conformance, rendering, packaging, or upstream -source builds. - -Use healthy existing endpoints when available. If OVRTX, Material, or Physics -endpoints are missing or unhealthy, run `deploy-content-agents` -first and do not continue until the shared standalone OVRTX renderer plus -independent Material and Physics service containers are healthy and exported -through `CONTENT_AGENTS_*_BASE_URL`. Deploy the Texture Agent too when texture -generation is requested. - -If required deployment authentication is missing, ask the user for -`NVIDIA_API_KEY` and wait. If a provided endpoint requires usage auth, ask for -the appropriate usage token instead. If deployment cannot produce healthy -services, report Content Agents readiness as blocked instead of proceeding to -conversion. +For conversion-only requests, set `property_assignment_intent=skip`, do not +deploy Content Agents, run `convert-to-usd`, then run `validate-usd-minimum` +on the generated USD if conversion succeeds. For validation-only requests, +set `property_assignment_intent=skip` and validate the USD the user +provided without rerunning conversion. + +Run `preflight` (or verify an existing `PHYSICAL_AI_PREFLIGHT_MANIFEST`) +before any converter, validation, Content Agents, OVRTX, packaging, or FET +step; treat it as dependency bootstrap, not workflow routing. Use +`--skip-content-agents` for conversion-only/validation-only requests. + +When `property_assignment_intent=run`, verify or deploy Content Agents +services immediately after confirming the source path and resolving intent, +before asset-context inspection, converter dependency checks, conversion, +validation, conformance, rendering, packaging, or upstream source builds. +Treat explicitly provided healthy endpoints as user-owned; otherwise run `deploy-content-agents`, +which deploys the shared standalone OVRTX renderer, then Material, Physics, +and optional Texture service containers in order. ## Instructions @@ -153,105 +126,47 @@ conversion. 14. Emit the consolidated workflow report with the final USD path, all stage reports, validation findings, rerun reasons, and next work. -Use the `simready-conform-profile` reference only after property assignment -when `property_assignment_intent=run`. It routes feature repair to upstream -SimReady Foundation FET skills such -as `simready-foundation-conform-fet-000-core`, -`simready-foundation-conform-fet-001-minimal`, -`simready-foundation-conform-fet-004-simulate-multi-body-physics`, and -`simready-foundation-conform-fet-005-simulate-grasp-physics` from branch -`main`. - -If `simready-validate` reports a repairable requirement after the first -conformance pass, feed the structured requirement IDs back into -the `simready-conform-profile` reference before writing the final result. In -particular, `GSP.001` is owned by upstream -`simready-foundation-conform-fet-005-simulate-grasp-physics`; run that skill when a -vision-capable agent can inspect visual evidence or explicit grasp points were -provided, otherwise record the FET005 step as blocked by missing vision/points -instead of treating it as an optional preview task. -For `RB.MB.001`, route the failure to -upstream `simready-foundation-conform-fet-004-simulate-multi-body-physics`. Do not assume -multiple visual prims are multiple rigid bodies; inspect -`UsdPhysics.RigidBodyAPI` applications. When the Physics Agent report shows -composed topology optimization or the USD has existing component colliders/part -roots and the profile validator reports FET004/RB.MB.001, FET004 should promote -those existing components into rigid bodies without creating geometry. Do not -mark the gate not applicable until after confirming there are fewer than two -reusable body candidates. - ## Output Format Emit a consolidated workflow report in Markdown, and include JSON when the -workflow writes structured artifacts. The report must include: - -- Overall status: `passed`, `blocked`, `failed`, or `needs_rerun`. -- Request summary: source asset path, detected source format, output root, - selected SimReady profile/version, and property assignment intent. -- Ordered stage results: stage reference, input artifact, output USD or USDZ - path, report path, status, blocker reason, and rerun reason when applicable. -- Content Agents readiness and property assignment results with service URLs, - tokens, and credentials redacted. -- Conformance and validation findings grouped by gate, requirement ID, selected - FET repair reference, repair-loop attempt, and final disposition. -- Final artifacts: final reported USD path, render preview path when requested, - package root and package validation report when packaging ran, Markdown report - path, JSON report path when present, and recommended next work. +workflow writes structured artifacts. Report overall status as `passed`, +`blocked`, `failed`, or `needs_rerun`. See `references/workflow.md` for the +required Markdown and JSON report fields. ## Detailed References Read only the references needed for the current request: - `references/preflight/README.md`: deterministic local setup, manifest/env - contract, Linux and Windows wrappers, Content Agents deployment opt-out, and - guardrail behavior. + contract, wrappers, deployment opt-out, and guardrail behavior. - `references/workflow.md`: inputs, source routing, detailed workflow, - validation policy, output report fields, approval points, and next steps. -- `references/commands.md`: concrete portable script command patterns for each - stage. + validation policy, output report fields, and next steps. +- `references/commands.md`: concrete portable script command patterns. - `references/assemble-package-source/README.md`: two-zone package source - assembly, canonical root USD naming, thumbnail placement, and self-contained - deliverable checks. + assembly, root USD naming, thumbnail placement, and deliverable checks. +- `references/troubleshooting.md`: symptom/cause/fix table plus FET + (`GSP.001`/`RB.MB.001`) repair-routing detail. +- `references/publishing-layout.md`: frontmatter compatibility-field notes and + layout rationale for this skill's own file tree. ## Publishing Layout Notes -Use `skills/omniverse-cad-to-simready/` as the source of truth for this product -repo's skill. The `.agents/skills` symlink is a compatibility alias for local -agentskills.io-style discovery, and `.codex/skills` and `.claude/skills` are -agent-specific compatibility aliases. - -Frontmatter keeps `version` and `tools` at top level for agentskills.io runtime -compatibility. NVCARPS discoverability fields live under `metadata`. - -The nested `references/` tree is intentional. It keeps one public catalog skill -while retaining script-bearing atomic stage references, upstream handoff notes, -and router documentation under the workflow. Do not flatten those references or -promote nested README references to sibling `SKILL.md` files unless the repo's -publishing model changes. +Use `skills/omniverse-cad-to-simready/` as the source of truth for this +product repo's skill. The `.agents/skills` symlink is a compatibility alias +for local agentskills.io-style discovery, and the nested `references/` tree +is intentional. See `references/publishing-layout.md` for the alias list, +frontmatter field placement, and flattening rules. ## Limitations - This workflow coordinates existing conversion, property assignment, conformance, validation, rendering, and packaging skills; it does not replace them with a single monolithic runner command. -- Stop at the first failing deployment, conversion, property-assignment, or - conformance authoring gate unless the user explicitly asks for best-effort - continuation. -- Upstream `simready-foundation-conform-fet-005-simulate-grasp-physics` needs visual - review or explicit grasp points before it can author a meaningful grasp - vector. ## Troubleshooting -| Symptom | Cause | Fix | -|---------|-------|-----| -| Downstream reference reports that cad-to-simready preflight has not prepared a component | `PHYSICAL_AI_REQUIRE_PREFLIGHT=1` is set, but the manifest is missing or the required runtime/service is not `ready` | Run `preflight/scripts/preflight.py`, source the generated env file, or explicitly disable service deployment with `--skip-content-agents` only when Content Agents are out of scope. | -| Workflow stops on `GSP.001` and reports the failure as unclassified | Visual evidence or explicit grasp points were not provided to FET005 | Run upstream `simready-foundation-conform-fet-005-simulate-grasp-physics` only after a vision-capable agent has reviewed the asset, or pass explicit grasp points. Otherwise report the FET005 step as `blocked`, not failed. | -| Validation fails after a meaningful USD artifact already exists | Workflow stopped at the first validation finding | Continue remaining diagnostic gates and mark the result `needs_rerun`. Do not stop at validation findings once a USD artifact has been produced. | -| Property-assignment stage fails with a missing service endpoint | Content Agents service was not deployed before conversion | Run `deploy-content-agents` first. Do not start asset inspection, conversion, validation, conformance, rendering, or packaging before Content Agents readiness when property assignment will run. | -| Material Agent reports that rendering produced `0 images` after unit or profile repair | A FET repair, commonly FET001 unit normalization, was applied before Material Agent and changed the USD layering/scene state consumed by the service | Rerun assignment from the converted/minimum-valid USD: Material Agent first, then Physics Agent, then run `simready-conform-profile` and FET repairs on the latest service-authored USD. | -| Material or Physics Agent local optimized path reports `Permission denied: '/app/.build-resources/scene_optimizer_core/python'` | Local Docker Scene Optimizer bundle permissions prevent the non-root service user from reading the packaged SO runtime | Repair the relevant local container with `docker exec --user root content-material-agent-service chmod -R a+rX /app/.build-resources/scene_optimizer_core` or `docker exec --user root content-physics-agent-service chmod -R a+rX /app/.build-resources/scene_optimizer_core`, then rerun the same optimized agent command. Do not treat the no-optimizer fallback as the root cause for instanced/prototype assets. | -| `RB.MB.001` fails even though the asset has many prims | The profile counts `UsdPhysics.RigidBodyAPI` prims, not visual or collider prims; Physics Agent may author one root rigid body | Route to upstream `simready-foundation-conform-fet-004-simulate-multi-body-physics`. First ensure Physics Agent used composed-topology optimization when applicable, then promote existing component colliders/part roots when the active profile reports FET004/RB.MB.001 and no geometry must be invented. | +Read `references/troubleshooting.md` only when a specific stage or validation +gate is failing; it owns the symptom/cause/fix table and FET repair routing. ## Hard Rules diff --git a/skills/omniverse-cad-to-simready/config/skillspector-baseline.yaml b/skills/omniverse-cad-to-simready/config/skillspector-baseline.yaml new file mode 100644 index 00000000..3c2fc5f2 --- /dev/null +++ b/skills/omniverse-cad-to-simready/config/skillspector-baseline.yaml @@ -0,0 +1,31 @@ +version: 2 +rules: +- id: "E2" + path: "references/convert-to-usd/references/usd-convert-cad/scripts/run.py" + message: "*os.environ.copy()*" + reason: "Reviewed: used only for the known local CAD converter subprocess; not logged or sent externally." +- id: "E2" + path: "references/preflight/scripts/preflight.py" + message: "*os.environ.copy()*" + reason: "Reviewed: used only for known local preflight, dependency, Docker Compose, and git subprocesses; secret values are not logged or sent externally." +- id: "E2" + path: "shared/simready_package.py" + message: "*os.environ.copy()*" + reason: "Reviewed: used only for the known local SimReady packaging subprocess; not logged or sent externally." +- id: "PE3" + path: "references/commands.md" + message: "*.[e]nv*" + reason: "Reviewed documentation references to a fixed generated configuration filename, not credential-file discovery." +- id: "PE3" + path: "references/deploy-content-agents/README.md" + message: "*.[e]nv*" + reason: "Reviewed documentation for an explicitly named deployment configuration file, not credential-file discovery." +- id: "PE3" + path: "references/preflight/README.md" + message: "*.[e]nv*" + reason: "Reviewed documentation references to an explicitly named deployment configuration file, not credential-file discovery." +- id: "PE3" + path: "references/preflight/scripts/preflight.py" + message: "*.[e]nv*" + reason: "Reviewed: textual matches are docstrings, filenames, and status messages; credential handling is limited to the caller-provided file, allowlisted variables, and owner-only permissions." +fingerprints: [] diff --git a/skills/omniverse-cad-to-simready/evals/evals.json b/skills/omniverse-cad-to-simready/evals/evals.json index 7ab92efa..4a47707d 100644 --- a/skills/omniverse-cad-to-simready/evals/evals.json +++ b/skills/omniverse-cad-to-simready/evals/evals.json @@ -1,60 +1,112 @@ -{ - "version": "0.1.0", - "skill": "omniverse-cad-to-simready", - "cases": [ - { - "id": "cad-to-simready-end-to-end-positive", - "question": "Take this STEP assembly from source CAD to a SimReady prop, including conversion, conformance, validation, and package metadata if possible.", - "expected_skill": "omniverse-cad-to-simready", - "expected_script": null, - "ground_truth": "The agent should select the CAD-to-SimReady workflow, treat it as an end-to-end source-asset to SimReady request, run or verify preflight first, and coordinate conversion, validation, conformance, optional property assignment, and packaging through nested references.", - "expected_behavior": [ - "Reads skills/omniverse-cad-to-simready/SKILL.md.", - "Classifies the request as an end-to-end CAD/source-asset to SimReady workflow.", - "Runs the preflight reference or verifies an existing PHYSICAL_AI_PREFLIGHT_MANIFEST before downstream stages.", - "Routes source conversion through references/convert-to-usd instead of inventing a monolithic runner.", - "Runs validation and conformance stages in workflow order and reports the first blocking gate." - ] - }, - { - "id": "cad-to-simready-conversion-only-positive", - "question": "Use the repo-local omniverse-cad-to-simready workflow on skills/omniverse-cad-to-simready/evals/files/minimal_mesh.stl. Skip property prediction and assignment. Convert it to USD and run minimum validation only.", - "expected_skill": "omniverse-cad-to-simready", - "expected_script": null, - "ground_truth": "The agent should keep this as a conversion-only CAD-to-SimReady workflow slice, skip Content Agents deployment, route the STL mesh source through convert-to-usd, and hand the generated USD to minimum validation.", - "expected_behavior": [ - "Reads skills/omniverse-cad-to-simready/SKILL.md.", - "Sets property_assignment_intent to skip because the prompt disables property prediction and assignment.", - "Uses preflight with Content Agents skipped or verifies an existing ready preflight manifest.", - "Runs or plans references/convert-to-usd/scripts/run.py for the STL input.", - "Runs or plans validate-usd-minimum on the converted USD and does not continue into full SimReady packaging." - ] - }, - { - "id": "cad-to-simready-viewer-distractor-negative", - "question": "Build a browser-based RTX USD viewer with camera controls, object picking, a stage tree, and render settings.", - "expected_skill": null, - "expected_script": null, - "ground_truth": "This is a realtime USD viewer application request, not a CAD/source-asset to SimReady conversion workflow.", - "expected_behavior": [ - "Does not select omniverse-cad-to-simready.", - "Does not run CAD conversion, SimReady conformance, or SimReady package creation steps.", - "Routes to a viewer-oriented skill if one is available." - ] - }, - { - "id": "cad-to-simready-content-agents-blocked", - "question": "Convert this source asset to a SimReady USD and use Content Agents to assign material and physics properties, but no Content Agents endpoints or NVIDIA_API_KEY are configured.", - "expected_skill": "omniverse-cad-to-simready", - "expected_script": null, - "ground_truth": "The agent should select the CAD-to-SimReady workflow, recognize that property assignment requires Content Agents readiness, and report the missing deployment/authentication blocker instead of claiming property assignment succeeded.", - "expected_behavior": [ - "Reads skills/omniverse-cad-to-simready/SKILL.md.", - "Sets property_assignment_intent to run because material and physics property assignment is requested.", - "Checks for healthy Content Agents endpoints or deployment credentials before conversion and downstream validation.", - "Reports missing Content Agents endpoints or NVIDIA_API_KEY as a blocker.", - "Does not claim material or physics assignment succeeded." - ] - } - ] -} +[ + { + "id": "cad-to-simready-end-to-end-positive", + "question": "Take this STEP assembly from source CAD to a SimReady prop, including conversion, conformance, validation, and package metadata if possible.", + "expected_skill": "omniverse-cad-to-simready", + "expected_script": null, + "ground_truth": "The agent should select the CAD-to-SimReady workflow, treat it as an end-to-end source-asset to SimReady request, run or verify preflight first, and coordinate conversion, validation, conformance, optional property assignment, and packaging through nested references.", + "expected_behavior": [ + "Reads skills/omniverse-cad-to-simready/SKILL.md.", + "Classifies the request as an end-to-end CAD/source-asset to SimReady workflow.", + "Runs the preflight reference or verifies an existing PHYSICAL_AI_PREFLIGHT_MANIFEST before downstream stages.", + "Routes source conversion through references/convert-to-usd instead of inventing a monolithic runner.", + "Runs validation and conformance stages in workflow order and reports the first blocking gate." + ] + }, + { + "id": "cad-to-simready-conversion-only-positive", + "question": "Use the repo-local omniverse-cad-to-simready workflow on skills/omniverse-cad-to-simready/evals/files/minimal_mesh.stl. Skip property prediction and assignment. Convert it to USD and run minimum validation only.", + "expected_skill": "omniverse-cad-to-simready", + "expected_script": null, + "ground_truth": "The agent should keep this as a conversion-only CAD-to-SimReady workflow slice, skip Content Agents deployment, route the STL mesh source through convert-to-usd, and hand the generated USD to minimum validation.", + "expected_behavior": [ + "Reads skills/omniverse-cad-to-simready/SKILL.md.", + "Sets property_assignment_intent to skip because the prompt disables property prediction and assignment.", + "Uses preflight with Content Agents skipped or verifies an existing ready preflight manifest.", + "Runs or plans references/convert-to-usd/scripts/run.py for the STL input.", + "Runs or plans validate-usd-minimum on the converted USD and does not continue into full SimReady packaging." + ] + }, + { + "id": "cad-to-simready-viewer-distractor-negative", + "question": "Build a browser-based RTX USD viewer with camera controls, object picking, a stage tree, and render settings.", + "expected_skill": null, + "expected_script": null, + "ground_truth": "This is a realtime USD viewer application request, not a CAD/source-asset to SimReady conversion workflow.", + "expected_behavior": [ + "Does not select omniverse-cad-to-simready.", + "Does not run CAD conversion, SimReady conformance, or SimReady package creation steps.", + "Routes to a viewer-oriented skill if one is available." + ] + }, + { + "id": "cad-to-simready-content-agents-blocked", + "question": "Convert this source asset to a SimReady USD and use Content Agents to assign material and physics properties, but no Content Agents endpoints or NVIDIA_API_KEY are configured.", + "expected_skill": "omniverse-cad-to-simready", + "expected_script": null, + "ground_truth": "The agent should select the CAD-to-SimReady workflow, recognize that property assignment requires Content Agents readiness, and report the missing deployment/authentication blocker instead of claiming property assignment succeeded.", + "expected_behavior": [ + "Reads skills/omniverse-cad-to-simready/SKILL.md.", + "Sets property_assignment_intent to run because material and physics property assignment is requested.", + "Checks for healthy Content Agents endpoints or deployment credentials before conversion and downstream validation.", + "Reports missing Content Agents endpoints or NVIDIA_API_KEY as a blocker.", + "Does not claim material or physics assignment succeeded." + ] + }, + { + "id": "cad-to-simready-content-agents-openai-gpt55", + "question": "Deploy Content Agents for the CAD-to-SimReady workflow using the private dotenv at ~/Codes/.env. Use OpenAI gpt-5.5 for both the VLM and LLM, deploy the required local services, and then run material and physics assignment.", + "expected_skill": "omniverse-cad-to-simready", + "expected_script": null, + "ground_truth": "The agent should select the CAD-to-SimReady workflow, run preflight with Content Agents enabled, load the OpenAI key from the private dotenv without printing it, configure both VLM and LLM backends as openai with model gpt-5.5, deploy or verify OVRTX before Material and Physics services, and only continue after healthy Content Agents endpoints are exported.", + "expected_behavior": [ + "Reads skills/omniverse-cad-to-simready/SKILL.md and the preflight or deploy-content-agents reference.", + "Uses --content-agents-secret-env-file with the provided private dotenv rather than asking the user to paste the key.", + "Sets both --content-agents-vlm-backend openai and --content-agents-llm-backend openai with model gpt-5.5.", + "Treats OpenAI as the selected hosted provider and does not route the request through NIM, NVCF, or build.nvidia.com defaults.", + "Deploys or verifies the shared standalone OVRTX renderer before deploying Material and Physics services one at a time." + ] + }, + { + "id": "cad-to-simready-content-agents-custom-openai-vlm", + "question": "For this SimReady conversion, deploy Content Agents with a custom OpenAI-compatible VLM endpoint at https://vlm.example.test/v1, model vision-router-prod, and API key environment variable ACME_VLM_KEY. Use Anthropic Claude for the LLM.", + "expected_skill": "omniverse-cad-to-simready", + "expected_script": null, + "ground_truth": "The agent should keep the custom OpenAI-compatible VLM endpoint on the openai backend, pass the custom endpoint and API-key environment variable through preflight, use the Anthropic backend and key for the LLM, deploy missing local services in deterministic order, and avoid treating a generic OpenAI-compatible endpoint as a NIM backend.", + "expected_behavior": [ + "Reads the Content Agents deployment guidance before configuring services.", + "Sets --content-agents-vlm-backend openai together with --content-agents-vlm-endpoint and --content-agents-vlm-api-key-env ACME_VLM_KEY.", + "Loads or asks for the custom ACME_VLM_KEY secret without printing it.", + "Sets the LLM backend to anthropic and uses the Anthropic provider key path for the LLM.", + "Does not infer nim for a custom OpenAI-compatible endpoint and does not require NVIDIA NIM credentials for that VLM." + ] + }, + { + "id": "cad-to-simready-content-agents-explicit-endpoints", + "question": "I already have healthy Content Agents endpoints: Material at http://material.local:8100, Physics at http://physics.local:8200, and OVRTX at http://render.local:8001. Use those for CAD-to-SimReady property assignment instead of deploying containers.", + "expected_skill": "omniverse-cad-to-simready", + "expected_script": null, + "ground_truth": "The agent should treat explicitly provided Material, Physics, and OVRTX endpoints as user-owned, probe them for readiness, export the corresponding Content Agents and renderer environment variables when healthy, and not replace them with managed local containers.", + "expected_behavior": [ + "Reads the preflight or deploy-content-agents reference before acting on the endpoints.", + "Treats provided CONTENT_AGENTS_*_BASE_URL and render endpoints as user-owned services.", + "Probes the provided endpoints and blocks if they are unhealthy instead of silently deploying replacements.", + "Exports healthy CONTENT_AGENTS_MATERIAL_AGENT_BASE_URL, CONTENT_AGENTS_PHYSICS_AGENT_BASE_URL, and RENDER_ENDPOINT values.", + "Continues to material and physics assignment only after endpoint readiness is verified." + ] + }, + { + "id": "cad-to-simready-content-agents-internal-backend-negative", + "question": "Deploy the CAD-to-SimReady Content Agents stack with the perflab or nvidia_inference backend for the VLM.", + "expected_skill": "omniverse-cad-to-simready", + "expected_script": null, + "ground_truth": "The agent should select the CAD-to-SimReady deployment path but reject internal-only Content Agents backend names in this public skill. It should ask for a public hosted backend such as openai, anthropic, gemini, nim for actual NIM endpoints, or a custom OpenAI-compatible endpoint configured through the openai backend.", + "expected_behavior": [ + "Reads the Content Agents deployment guidance before accepting backend names.", + "Does not use perflab, perflab_azure_openai, nvidia_inference, or other internal-only backend defaults.", + "Offers public provider choices such as openai, anthropic, gemini, or nim for actual NIM endpoints.", + "Explains that custom OpenAI-compatible endpoints should use the openai backend plus explicit endpoint and API-key-env settings.", + "Does not deploy services until a supported public backend or explicit healthy endpoint is selected." + ] + } +] diff --git a/skills/omniverse-cad-to-simready/references/assemble-package-source/README.md b/skills/omniverse-cad-to-simready/references/assemble-package-source/README.md index 79be9761..b46f6561 100644 --- a/skills/omniverse-cad-to-simready/references/assemble-package-source/README.md +++ b/skills/omniverse-cad-to-simready/references/assemble-package-source/README.md @@ -30,18 +30,23 @@ Collect: ## Instructions 1. Confirm `final_usd` and `thumbnail` exist. -2. Create `{output_root}/deliverable/simready_usd/`. -3. Copy `final_usd` to the canonical root USD path: +2. Inspect the thumbnail pixels and stop before writing anything when it is + blank/uniform or when inspection is unavailable. This stage always fails + closed; the `--no-fail-on-uniform` diagnostic opt-out on + `ovrtx-render-service` has no equivalent here because every thumbnail + assembled here ships inside the deliverable. +3. Create `{output_root}/deliverable/simready_usd/`. +4. Copy `final_usd` to the canonical root USD path: `simready_usd/sm_{asset_name}_01.usd`. -4. Inspect USD layers and authored asset paths with OpenUSD APIs. -5. Copy local USD layer dependencies, textures, MDL files, and sidecar assets +5. Inspect USD layers and authored asset paths with OpenUSD APIs. +6. Copy local USD layer dependencies, textures, MDL files, and sidecar assets into `deliverable/simready_usd/`. -6. Rewrite package-local asset paths in copied USD layers. -7. Copy the thumbnail to +7. Rewrite package-local asset paths in copied USD layers. +8. Copy the thumbnail to `simready_usd/.thumbs/256x256/{root_usd_filename}.png`. -8. Run a self-containment check over USD composition dependencies and authored +9. Run a self-containment check over USD composition dependencies and authored `Sdf.AssetPath` values. -9. Write a JSON assembly report. +10. Write a JSON assembly report. Do not pass the workflow output root to `nv-core-package-sample`. Pass `{output_root}/deliverable` with root USD @@ -84,6 +89,7 @@ The report includes: - `root_usd_path` - `root_usd_relative_path` - `thumbnail_path` +- `thumbnail_inspection` - `copied_files` - `rewritten_paths` - `checks` @@ -96,6 +102,8 @@ The report includes: Fail when: - the final USD or thumbnail is missing +- the thumbnail is blank or uniform by pixel inspection +- thumbnail pixel inspection is unavailable, for example when Pillow is missing - OpenUSD cannot open the final assembled root USD - a local authored asset path cannot be resolved - an authored dependency resolves outside `deliverable/` diff --git a/skills/omniverse-cad-to-simready/references/assemble-package-source/scripts/check_dependencies.py b/skills/omniverse-cad-to-simready/references/assemble-package-source/scripts/check_dependencies.py index 4a319491..c14666ad 100644 --- a/skills/omniverse-cad-to-simready/references/assemble-package-source/scripts/check_dependencies.py +++ b/skills/omniverse-cad-to-simready/references/assemble-package-source/scripts/check_dependencies.py @@ -5,11 +5,29 @@ from __future__ import annotations import json +from pathlib import Path import sys +sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "shared")) + +from preflight_manifest import preflight_required, preflight_status_check + + +SKILL = "assemble-package-source" + def main() -> int: checks: list[dict[str, object]] = [] + if preflight_required(): + preflight_check = preflight_status_check(SKILL, "openusd_python") + if not preflight_check["passed"]: + payload = { + "skill": SKILL, + "passed": False, + "checks": [preflight_check], + } + print(json.dumps(payload, indent=2, sort_keys=True)) + return 1 try: from pxr import Sdf, Usd, UsdUtils # noqa: F401 except Exception as exc: @@ -29,6 +47,25 @@ def main() -> int: } ) + try: + from PIL import Image, ImageStat # noqa: F401 + except Exception as exc: + checks.append( + { + "name": "pillow_available", + "passed": False, + "message": f"Pillow is unavailable: {exc}. Thumbnail pixel inspection cannot run, so scripts/run.py will fail assembly closed.", + } + ) + else: + checks.append( + { + "name": "pillow_available", + "passed": True, + "message": "Pillow is available for thumbnail pixel inspection", + } + ) + payload = { "skill": "assemble-package-source", "passed": all(bool(check["passed"]) for check in checks), diff --git a/skills/omniverse-cad-to-simready/references/assemble-package-source/scripts/run.py b/skills/omniverse-cad-to-simready/references/assemble-package-source/scripts/run.py index d26003a4..09231960 100644 --- a/skills/omniverse-cad-to-simready/references/assemble-package-source/scripts/run.py +++ b/skills/omniverse-cad-to-simready/references/assemble-package-source/scripts/run.py @@ -15,7 +15,9 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "shared")) -from script_utils import check_result_with_code as _check +from script_utils import check_result_with_code as _check, inspect_png + +from preflight_manifest import preflight_required, preflight_status_check USD_SUFFIXES = {".usd", ".usda", ".usdc"} @@ -312,6 +314,21 @@ def _self_containment_checks(deliverable_root: Path, simready_root: Path) -> lis return checks +def _thumbnail_pixel_checks(thumbnail: Path, inspection: dict[str, Any]) -> list[dict[str, Any]]: + # Same fail-closed rule as the ovrtx-render-service render gate: pixel + # inspection is what proves the thumbnail is not blank, so an unavailable + # inspection (missing Pillow, unreadable PNG, ...) is a blocker rather than + # a skip. There is deliberately no --no-fail-on-uniform opt-out here: that + # flag exists on the render stage for diagnostic-only runs, while every + # thumbnail assembled here ships inside the deliverable. + if inspection.get("available") is False: + message = inspection.get("warning", "Could not inspect thumbnail pixels") + return [_check("thumbnail_pixel_inspected", False, str(message), code="SR.002")] + if inspection.get("uniform"): + return [_check("thumbnail_non_uniform", False, f"Thumbnail is blank/uniform by pixel inspection: {thumbnail}", code="SR.002")] + return [_check("thumbnail_non_uniform", True, f"Thumbnail has visible pixel variation: {thumbnail}", "info", code="SR.002")] + + def _errors_from_checks(checks: list[dict[str, Any]]) -> list[str]: return [check["message"] for check in checks if check["severity"] == "error" and not check["passed"]] @@ -358,8 +375,18 @@ def assemble(args: argparse.Namespace) -> dict[str, Any]: "next_step": "fix-assembly-inputs", } checks = report["checks"] + if preflight_required(): + preflight_check = preflight_status_check("assemble-package-source", "openusd_python") + if not preflight_check["passed"]: + report["errors"] = [preflight_check["message"]] + _write_report(report_path, report) + return report checks.append(_check("final_usd_exists", final_usd.is_file(), f"Final USD exists: {final_usd}" if final_usd.is_file() else f"Final USD does not exist: {final_usd}")) checks.append(_check("thumbnail_exists", args.thumbnail.is_file(), f"Thumbnail exists: {args.thumbnail}" if args.thumbnail.is_file() else f"Thumbnail does not exist: {args.thumbnail}", code="SR.002")) + if args.thumbnail.is_file(): + thumbnail_inspection = inspect_png(args.thumbnail) + report["thumbnail_inspection"] = thumbnail_inspection + checks.extend(_thumbnail_pixel_checks(args.thumbnail, thumbnail_inspection)) if _errors_from_checks(checks): report["errors"] = _errors_from_checks(checks) _write_report(report_path, report) diff --git a/skills/omniverse-cad-to-simready/references/commands.md b/skills/omniverse-cad-to-simready/references/commands.md index 7b97960a..53115ca4 100644 --- a/skills/omniverse-cad-to-simready/references/commands.md +++ b/skills/omniverse-cad-to-simready/references/commands.md @@ -26,7 +26,7 @@ python3 /path/to/skills/omniverse-cad-to-simready/references/content-agents/scri --call material \ --call physics \ --prompt "$ASSET_CONTEXT_PROMPT" \ - --convert-output-to-usd \ + --convert-physics-output-to-usd \ --report /path/to/output_dir/assignment/content-agents.json python3 /path/to/skills/omniverse-cad-to-simready/references/simready-conform-profile/scripts/run.py \ diff --git a/skills/omniverse-cad-to-simready/references/content-agents/README.md b/skills/omniverse-cad-to-simready/references/content-agents/README.md index 0787348c..4d139098 100644 --- a/skills/omniverse-cad-to-simready/references/content-agents/README.md +++ b/skills/omniverse-cad-to-simready/references/content-agents/README.md @@ -141,6 +141,11 @@ python3 /path/to/skills/omniverse-cad-to-simready/references/content-agents/refe --report output_dir/content-agents/material-agent-client.json ``` +The Material Agent wrapper sends `vlm_max_workers=1` and omits the service-side +`render` step by default. Use `--vlm-max-workers`, `--render-num-workers`, or +`--material-steps` only when the selected service run needs different +concurrency or its own render artifact. + For any existing endpoint that requires a different bearer token, set `MATERIAL_AGENT_TOKEN`, `PHYSICS_AGENT_TOKEN`, or `TEXTURE_AGENT_TOKEN` in the environment for the selected service instead of changing wrapper defaults. Use @@ -238,8 +243,8 @@ The router summary should include: | Service call fails because endpoint requires bearer auth and the wrapper's default token is wrong | Endpoint-specific credentials are not the wrapper's default environment token | Export the selected service token environment variable before running the command. Avoid `--token` except when environment injection is impossible. | | Required service endpoint missing | `CONTENT_AGENTS_*_BASE_URL` is unset | Hand off to `deploy-content-agents` for the missing target, then return to this router and rerun. | | Material Agent renders `0 images` after a pre-assignment FET repair | The input USD was normalized or otherwise rewritten before Material Agent, which can change layer composition or scene traversal behavior seen by the service | Restart Content Agents from the converted/minimum-valid USD, then run `simready-conform-profile` and FET fixes on the latest service-authored USD. | -| Material Agent local logs show `Scene Optimizer failed — continuing pipeline without optimization` and `Permission denied: '/app/.build-resources/scene_optimizer_core/python'` | Local Docker Scene Optimizer bundle parent directory is not traversable by the non-root `material-agent` user; tracked upstream in `NVIDIA-dev/world-understanding#303` | For the running local container, run `docker exec --user root content-material-agent-service chmod -R a+rX /app/.build-resources/scene_optimizer_core`, then rerun the same optimized Material Agent command. If the service is remote or cannot be repaired, include the optimizer-bypass evidence in the handoff. | -| Physics Agent fails immediately in `optimize_usd` with no completed steps, and local service logs show `Permission denied: '/app/.build-resources/scene_optimizer_core/python'` | Local Docker Scene Optimizer bundle parent directory is not traversable by the non-root `physics-agent` user; tracked upstream in `NVIDIA-dev/world-understanding#303` | For the running local container, run `docker exec --user root content-physics-agent-service chmod -R a+rX /app/.build-resources/scene_optimizer_core`, then rerun the same optimized Physics Agent command. If the service is remote or cannot be repaired, report the optimized attempt as blocked. | +| Material Agent local logs show `Scene Optimizer failed — continuing pipeline without optimization` and `Permission denied: '/app/.build-resources/scene_optimizer_core/python'` | Local Docker Scene Optimizer bundle parent directory is not traversable by the non-root `material-agent` user; tracked upstream in the `nvidia-omniverse/content-agents` issue tracker | For the running local container, run `docker exec --user root content-material-agent-service chmod -R a+rX /app/.build-resources/scene_optimizer_core`, then rerun the same optimized Material Agent command. If the service is remote or cannot be repaired, include the optimizer-bypass evidence in the handoff. | +| Physics Agent fails immediately in `optimize_usd` with no completed steps, and local service logs show `Permission denied: '/app/.build-resources/scene_optimizer_core/python'` | Local Docker Scene Optimizer bundle parent directory is not traversable by the non-root `physics-agent` user; tracked upstream in the `nvidia-omniverse/content-agents` issue tracker | For the running local container, run `docker exec --user root content-physics-agent-service chmod -R a+rX /app/.build-resources/scene_optimizer_core`, then rerun the same optimized Physics Agent command. If the service is remote or cannot be repaired, report the optimized attempt as blocked. | | Selected Content Agents call fails or times out | Service-level failure | Inspect the specific call's JSON report and retry that service reference after fixing the service or input issue. Do not hand-author substitute material/physics bindings. | | Physics Agent appears slow during `identify_asset`, `build_dataset_usd`, `predict`, `restore_usd`, or `apply_physics` | Remote rendering, VLM inference, or CAD/instanced topology can make valid service steps take many minutes | Keep waiting while the wrapper polls and the service remains non-terminal with changing status/progress. Do not terminate the wrapper and write a substitute report. | diff --git a/skills/omniverse-cad-to-simready/references/content-agents/references/material-agent-client/README.md b/skills/omniverse-cad-to-simready/references/content-agents/references/material-agent-client/README.md index d5a33f30..19fa1090 100644 --- a/skills/omniverse-cad-to-simready/references/content-agents/references/material-agent-client/README.md +++ b/skills/omniverse-cad-to-simready/references/content-agents/references/material-agent-client/README.md @@ -10,11 +10,11 @@ This reference does not assign rigid bodies, colliders, mass, friction, or textu Use the upstream NVIDIA Omniverse Content Agents Material Agent client skill as the authoritative reference for service API behavior, endpoint semantics, request fields, and client-side troubleshooting: -- Upstream skill: `https://github.com/nvidia-omniverse/content-agents/blob/main/.codex/skills/material-agent-client/SKILL.md` -- Upstream repository: `https://github.com/nvidia-omniverse/content-agents` on branch `main` -- Upstream service client: `https://github.com/nvidia-omniverse/content-agents/blob/main/apps/material_agent_service/client/client.py` +- Upstream skill: `https://github.com/nvidia-omniverse/content-agents/blob/v0.5.2/.codex/skills/material-agent-client/SKILL.md` +- Upstream repository: `https://github.com/nvidia-omniverse/content-agents` at manifest pin `v0.5.2` +- Upstream service client: `https://github.com/nvidia-omniverse/content-agents/blob/v0.5.2/apps/material_agent_service/client/client.py` -Access note: Browser or raw-file fetches of the upstream skill URL can fail. If that happens, use the normalized local clone of `https://github.com/nvidia-omniverse/content-agents` checked out to `main` and read `.codex/skills/material-agent-client/SKILL.md` from that checkout. Resolve that clone from `CONTENT_AGENTS_UPSTREAM_ROOT`, then `$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/content-agents`, then `$HOME/.physical-ai-skill-hub/upstreams/content-agents`. +Access note: Browser or raw-file fetches of the upstream skill URL can fail. If that happens, use the normalized local clone of `https://github.com/nvidia-omniverse/content-agents` checked out to the exact ref in `upstream-versions.lock.json` and read `.codex/skills/material-agent-client/SKILL.md` from that checkout. Resolve that clone from `CONTENT_AGENTS_UPSTREAM_ROOT`, then `$OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT/content-agents`, then `$HOME/.omniverse-cad-to-simready/upstreams/content-agents`. Do not copy or reinterpret upstream Material Agent API behavior here. Keep this reference limited to this reference's wrapper contract, required environment, report shape, and downstream handoff. @@ -63,6 +63,9 @@ Collect: | `prompt` | Optional material assignment guidance; prefer the `material_physics_prompt` from `identify-asset-context` when available. | | `optimize_usd` | Optional; enabled by default. The wrapper inspects USD topology first and disables it only for instance/prototype-only assets that would otherwise lose all renderable prims if the main service falls back to original topology. If system `python3` cannot import `pxr`, the wrapper tries `uv run --python 3.12` for topology inspection before falling back to the service default. | | `skip_instances` | Optional; normally left to the service default when `optimize_usd` remains enabled. The wrapper sends `skip_instances=false` only when it selects the instance/prototype-only traversal path. | +| `vlm_max_workers` | Optional Material Agent VLM worker limit. The wrapper defaults to `1` to avoid partial prediction batches from bursty service-side VLM concurrency. | +| `render_num_workers` | Optional Material Agent render worker limit passed through to the service when supplied. | +| `material_steps` | Optional comma-separated service steps. The wrapper defaults to `build_dataset_usd,build_dataset_prepare_dataset,predict,apply`; final rendering is handled by `ovrtx-render-service` after material assignment. | ## Upload Prep @@ -120,8 +123,8 @@ docker exec --user root content-material-agent-service chmod -R a+rX /app/.build If the container name differs, use the active Material Agent service container. If the endpoint is managed or remote and you cannot inspect or repair the container, preserve the report and include the optimizer-bypass evidence in the -blocked or failed handoff. Track the upstream issue at -`https://github.com/NVIDIA-dev/world-understanding/issues/303`. +blocked or failed handoff. Track the upstream issue in the +`https://github.com/nvidia-omniverse/content-agents` issue tracker. ## Instructions @@ -130,7 +133,7 @@ blocked or failed handoff. Track the upstream issue at 3. If no endpoint is available, check `NVIDIA_API_KEY`; if missing, ask the user to provide one and wait. 4. Use `deploy-content-agents` with target `material`; when deployment succeeds and the service is healthy, set `CONTENT_AGENTS_MATERIAL_AGENT_BASE_URL` and return to this workflow. 5. If an asset context report exists, use its likely identity, evidence, and material hints to craft `--prompt`. -6. Run this reference's portable `scripts/run.py`. It inspects the USD first, strips missing MDL shader-source upload references when needed, keeps `optimize_usd=true` by default, and switches to `optimize_usd=false` with `skip_instances=false` only for instance/prototype-only topology where the main service would otherwise skip all renderable prims. If the optimized path still fails with the known `Rendering produced 0 images` symptom, the wrapper retries once with `optimize_usd=false` and `skip_instances=false` and records both attempts. After downloading the materialized USD, it removes unbound stale material subtrees and repairs bound stale shaders with broken `sourceAsset` references. +6. Run this reference's portable `scripts/run.py`. It inspects the USD first, strips missing MDL shader-source upload references when needed, keeps `optimize_usd=true` by default, sends `vlm_max_workers=1`, omits the service-side `render` step, and switches to `optimize_usd=false` with `skip_instances=false` only for instance/prototype-only topology where the main service would otherwise skip all renderable prims. If the optimized path still fails with the known `Rendering produced 0 images` symptom, the wrapper retries once with `optimize_usd=false` and `skip_instances=false` and records both attempts. After downloading the materialized USD, it removes unbound stale material subtrees and repairs bound stale shaders with broken `sourceAsset` references. 7. If the optimized path was intended but service logs show the local Scene Optimizer permission failure above, repair the container permissions and rerun the optimized command before accepting a direct-traversal result as diagnostic. 8. Preserve the JSON report, materialized USD output, predictions JSONL, and report HTML when available. 9. Use `output_usd_path` from the report as the input to `physics-agent-client`. @@ -158,6 +161,10 @@ Use `--no-material-output-cleanup` only when preserving the raw downloaded Material Agent artifact is more important than downstream SimReady validation hygiene. +Use `--material-steps` only when the service's own render artifact is required. +The CAD-to-SimReady workflow treats the standalone `ovrtx-render-service` output +as the final render. + ## Output Format The report includes: diff --git a/skills/omniverse-cad-to-simready/references/content-agents/references/physics-agent-client/README.md b/skills/omniverse-cad-to-simready/references/content-agents/references/physics-agent-client/README.md index 81d6009c..6944eb59 100644 --- a/skills/omniverse-cad-to-simready/references/content-agents/references/physics-agent-client/README.md +++ b/skills/omniverse-cad-to-simready/references/content-agents/references/physics-agent-client/README.md @@ -10,11 +10,11 @@ This reference should be the main bridge between Content Agents property predict Use the upstream NVIDIA Omniverse Content Agents Physics Agent client skill as the authoritative reference for service API behavior, endpoint semantics, request fields, and client-side troubleshooting: -- Upstream skill: `https://github.com/nvidia-omniverse/content-agents/blob/main/.codex/skills/physics-agent-client/SKILL.md` -- Upstream repository: `https://github.com/nvidia-omniverse/content-agents` on branch `main` -- Upstream service client: `https://github.com/nvidia-omniverse/content-agents/blob/main/apps/physics_agent_service/client/client.py` +- Upstream skill: `https://github.com/nvidia-omniverse/content-agents/blob/v0.5.2/.codex/skills/physics-agent-client/SKILL.md` +- Upstream repository: `https://github.com/nvidia-omniverse/content-agents` at manifest pin `v0.5.2` +- Upstream service client: `https://github.com/nvidia-omniverse/content-agents/blob/v0.5.2/apps/physics_agent_service/client/client.py` -Access note: Browser or raw-file fetches of the upstream skill URL can fail. If that happens, use the normalized local clone of `https://github.com/nvidia-omniverse/content-agents` checked out to `main` and read `.codex/skills/physics-agent-client/SKILL.md` from that checkout. Resolve that clone from `CONTENT_AGENTS_UPSTREAM_ROOT`, then `$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/content-agents`, then `$HOME/.physical-ai-skill-hub/upstreams/content-agents`. +Access note: Browser or raw-file fetches of the upstream skill URL can fail. If that happens, use the normalized local clone of `https://github.com/nvidia-omniverse/content-agents` checked out to the exact ref in `upstream-versions.lock.json` and read `.codex/skills/physics-agent-client/SKILL.md` from that checkout. Resolve that clone from `CONTENT_AGENTS_UPSTREAM_ROOT`, then `$OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT/content-agents`, then `$HOME/.omniverse-cad-to-simready/upstreams/content-agents`. Do not copy or reinterpret upstream Physics Agent API behavior here. Keep this reference limited to this reference's wrapper contract, required environment, report shape, and downstream handoff. @@ -129,8 +129,8 @@ docker exec --user root content-physics-agent-service chmod -R a+rX /app/.build- If the container name differs, use the active Physics Agent service container. If the endpoint is managed or remote and you cannot inspect or repair the container, report the optimizer failure as blocked and include the optimized -attempt details. Track the upstream issue at -`https://github.com/NVIDIA-dev/world-understanding/issues/303`. +attempt details. Track the upstream issue in the +`https://github.com/nvidia-omniverse/content-agents` issue tracker. ## Instructions diff --git a/skills/omniverse-cad-to-simready/references/content-agents/references/texture-agent-client/README.md b/skills/omniverse-cad-to-simready/references/content-agents/references/texture-agent-client/README.md index 6d0ab59d..edbcfc09 100644 --- a/skills/omniverse-cad-to-simready/references/content-agents/references/texture-agent-client/README.md +++ b/skills/omniverse-cad-to-simready/references/content-agents/references/texture-agent-client/README.md @@ -10,11 +10,11 @@ Texture generation is optional for the current `omniverse-cad-to-simready` path. Use the upstream NVIDIA Omniverse Content Agents Texture Agent client skill as the authoritative reference for service API behavior, endpoint semantics, request fields, and client-side troubleshooting: -- Upstream skill: `https://github.com/nvidia-omniverse/content-agents/blob/main/.codex/skills/texture-agent-client/SKILL.md` -- Upstream repository: `https://github.com/nvidia-omniverse/content-agents` on branch `main` -- Upstream service client: `https://github.com/nvidia-omniverse/content-agents/blob/main/apps/texture_agent_service/client/client.py` +- Upstream skill: `https://github.com/nvidia-omniverse/content-agents/blob/v0.5.2/.codex/skills/texture-agent-client/SKILL.md` +- Upstream repository: `https://github.com/nvidia-omniverse/content-agents` at manifest pin `v0.5.2` +- Upstream service client: `https://github.com/nvidia-omniverse/content-agents/blob/v0.5.2/apps/texture_agent_service/client/client.py` -Access note: Browser or raw-file fetches of the upstream skill URL can fail. If that happens, use the normalized local clone of `https://github.com/nvidia-omniverse/content-agents` checked out to `main` and read `.codex/skills/texture-agent-client/SKILL.md` from that checkout. Resolve that clone from `CONTENT_AGENTS_UPSTREAM_ROOT`, then `$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/content-agents`, then `$HOME/.physical-ai-skill-hub/upstreams/content-agents`. +Access note: Browser or raw-file fetches of the upstream skill URL can fail. If that happens, use the normalized local clone of `https://github.com/nvidia-omniverse/content-agents` checked out to the exact ref in `upstream-versions.lock.json` and read `.codex/skills/texture-agent-client/SKILL.md` from that checkout. Resolve that clone from `CONTENT_AGENTS_UPSTREAM_ROOT`, then `$OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT/content-agents`, then `$HOME/.omniverse-cad-to-simready/upstreams/content-agents`. Do not copy or reinterpret upstream Texture Agent API behavior here. Keep this reference limited to this reference's wrapper contract, required environment, report shape, and workflow path selection. diff --git a/skills/omniverse-cad-to-simready/references/content-agents/scripts/content_agent_client.py b/skills/omniverse-cad-to-simready/references/content-agents/scripts/content_agent_client.py index 4c5fff10..2c0962da 100644 --- a/skills/omniverse-cad-to-simready/references/content-agents/scripts/content_agent_client.py +++ b/skills/omniverse-cad-to-simready/references/content-agents/scripts/content_agent_client.py @@ -15,7 +15,7 @@ import time from typing import Any from urllib.error import HTTPError, URLError -from urllib.parse import urljoin +from urllib.parse import urljoin, urlparse, urlunparse from urllib.request import Request, urlopen import uuid @@ -29,6 +29,8 @@ USD_EXTENSIONS = {".usd", ".usda", ".usdc", ".usdz"} USD_LAYER_EXTENSIONS = {".usd", ".usda", ".usdc"} +MATERIAL_AGENT_DEFAULT_STEPS = "build_dataset_usd,build_dataset_prepare_dataset,predict,apply" +DEFAULT_MATERIAL_AGENT_VLM_MAX_WORKERS = 1 TERMINAL_SUCCESS = {"completed", "complete", "succeeded", "success", "done"} TERMINAL_FAILURE = {"failed", "failure", "cancelled", "canceled", "error"} MATERIAL_ZERO_IMAGE_MARKERS = ( @@ -51,6 +53,11 @@ "pash-e2e-physics_agent_service", "physics_agent_service", ) +LOCAL_OVRTX_ENDPOINT_ENV = ( + "OVRTX_RENDER_ENDPOINT", + "OVRTX_RENDER_BASE_URL", + "CONTENT_AGENTS_RENDER_BASE_URL", +) MDL_UPLOAD_PREP_SNIPPET = r""" import json import shutil @@ -790,6 +797,77 @@ def _required_output_path(agent_key: str, spec: dict[str, Any], asset_path: Path return output_directory / f"{asset_path.stem}{spec['output_suffix']}" +def _render_endpoint_candidates() -> list[tuple[str, str]]: + candidates: list[tuple[str, str]] = [] + for name in LOCAL_OVRTX_ENDPOINT_ENV: + value = os.environ.get(name) + if value: + candidates.append((name, value.strip())) + manifest, _, _ = load_preflight_manifest() + manifest_endpoint = ready_service_url(manifest, "ovrtx") + if manifest_endpoint: + candidates.append(("preflight_manifest_ovrtx", manifest_endpoint)) + + seen: set[str] = set() + unique: list[tuple[str, str]] = [] + for source, endpoint in candidates: + endpoint = endpoint.rstrip("/") + if not endpoint or endpoint in seen: + continue + seen.add(endpoint) + unique.append((source, endpoint)) + return unique + + +def _host_probe_render_endpoint(endpoint: str) -> str: + parsed = urlparse(endpoint) + if parsed.hostname != "host.docker.internal": + return endpoint + port = f":{parsed.port}" if parsed.port else "" + netloc = f"localhost{port}" + return urlunparse(parsed._replace(netloc=netloc)) + + +def _render_health_url(endpoint: str) -> str: + endpoint = _host_probe_render_endpoint(endpoint).rstrip("/") + parsed = urlparse(endpoint) + path = parsed.path.rstrip("/") + if path.endswith("/render"): + path = path[: -len("/render")] + base = urlunparse(parsed._replace(path=path, params="", query="", fragment="")).rstrip("/") + return urljoin(f"{base}/", "health") + + +def _is_local_render_endpoint(endpoint: str) -> bool: + host = (urlparse(endpoint).hostname or "").lower() + return host in {"localhost", "127.0.0.1", "0.0.0.0", "host.docker.internal", "::1"} + + +def _local_render_endpoint_check(request_timeout: float) -> dict[str, Any] | None: + for source, endpoint in _render_endpoint_candidates(): + if not _is_local_render_endpoint(endpoint): + continue + health_url = _render_health_url(endpoint) + try: + with urlopen(health_url, timeout=max(1.0, min(float(request_timeout), 10.0))) as response: + ok = 200 <= int(response.status) < 300 + except Exception as exc: + return _check( + "ovrtx_render_endpoint_reachable", + False, + f"Local OVRTX render endpoint from {source} is not reachable at {health_url}: {exc}", + ) + return _check( + "ovrtx_render_endpoint_reachable", + ok, + f"Local OVRTX render endpoint from {source} responded at {health_url}" + if ok + else f"Local OVRTX render endpoint from {source} did not return HTTP 2xx at {health_url}", + "info" if ok else "error", + ) + return None + + def _service_fields(args: argparse.Namespace, agent_key: str) -> dict[str, str]: fields: dict[str, str] = {} if agent_key == "material": @@ -809,6 +887,12 @@ def _service_fields(args: argparse.Namespace, agent_key: str) -> dict[str, str]: fields["skip_existing_materials"] = "true" if args.layer_only: fields["layer_only"] = "true" + if args.material_steps: + fields["steps"] = args.material_steps + if args.vlm_max_workers is not None: + fields["vlm_max_workers"] = str(args.vlm_max_workers) + if args.render_num_workers is not None: + fields["render_num_workers"] = str(args.render_num_workers) elif agent_key == "physics": if args.prompt: fields["user_prompt"] = args.prompt @@ -1227,6 +1311,16 @@ def _maybe_retry_physics_without_optimizer(report: dict[str, Any], args: argpars return retry_report +def _positive_int_arg(value: str) -> int: + try: + parsed = int(value) + except ValueError as exc: + raise argparse.ArgumentTypeError(f"{value!r} is not an integer") from exc + if parsed < 1: + raise argparse.ArgumentTypeError(f"{value!r} must be >= 1") + return parsed + + def _report_markdown(report: dict[str, Any]) -> str: lines = [ f"# {report['skill']} Report", @@ -1348,6 +1442,12 @@ def _command( command.append("--layer-only") if not args.material_output_cleanup: command.append("--no-material-output-cleanup") + if args.material_steps: + command.extend(["--material-steps", args.material_steps]) + if args.vlm_max_workers is not None: + command.extend(["--vlm-max-workers", str(args.vlm_max_workers)]) + if args.render_num_workers is not None: + command.extend(["--render-num-workers", str(args.render_num_workers)]) elif agent_key == "physics": if args.render_backend: command.extend(["--render-backend", args.render_backend]) @@ -1495,6 +1595,15 @@ def run(args: argparse.Namespace) -> dict[str, Any]: report["errors"] = [check["message"] for check in checks if check["severity"] == "error" and not check["passed"]] return report + if agent_key in {"material", "physics"}: + render_check = _local_render_endpoint_check(args.request_timeout) + if render_check is not None: + checks.append(render_check) + if not render_check["passed"]: + report["status"] = "BLOCKED" + report["errors"] = [render_check["message"]] + return report + if agent_key == "texture" and args.material_textures: try: json.loads(args.material_textures) @@ -1609,7 +1718,6 @@ def run(args: argparse.Namespace) -> dict[str, Any]: for warning in poll_warnings: report["warnings"].append(f"Recovered transient status poll error: {warning}") state = str(service_status.get("status", "")).lower() - checks.append(_check("session_completed", state in TERMINAL_SUCCESS, f"Content Agents session status: {service_status.get('status')}")) try: report["service_results"] = _json_request( "GET", @@ -1619,6 +1727,16 @@ def run(args: argparse.Namespace) -> dict[str, Any]: ) except Exception as exc: report["warnings"].append(f"Could not fetch service results: {exc}") + session_message = f"Content Agents session status: {service_status.get('status')}" + if state not in TERMINAL_SUCCESS: + service_results = report.get("service_results") + error_message = service_results.get("error_message") if isinstance(service_results, dict) else None + if error_message: + session_message = f"{session_message}: {error_message}" + failed_step = service_results.get("failed_step") + if failed_step: + session_message = f"{session_message} (failed_step: {failed_step})" + checks.append(_check("session_completed", state in TERMINAL_SUCCESS, session_message)) except Exception as exc: checks.append(_check("service_pipeline_completed", False, str(exc))) report["errors"] = [check["message"] for check in checks if check["severity"] == "error" and not check["passed"]] @@ -1793,6 +1911,27 @@ def _parser() -> argparse.ArgumentParser: parser.add_argument("--skip-prototypes", action="store_true") parser.add_argument("--skip-existing-materials", action="store_true") parser.add_argument("--layer-only", action="store_true") + parser.add_argument( + "--material-steps", + default=MATERIAL_AGENT_DEFAULT_STEPS, + help=( + "Comma-separated Material Agent service steps. The default omits the " + "service-side render step because this workflow performs the final " + "OVRTX render separately." + ), + ) + parser.add_argument( + "--vlm-max-workers", + type=_positive_int_arg, + default=DEFAULT_MATERIAL_AGENT_VLM_MAX_WORKERS, + help="Maximum parallel Material Agent VLM workers sent to the service.", + ) + parser.add_argument( + "--render-num-workers", + type=_positive_int_arg, + default=None, + help="Optional maximum parallel Material Agent render workers sent to the service.", + ) parser.add_argument( "--no-material-output-cleanup", dest="material_output_cleanup", diff --git a/skills/omniverse-cad-to-simready/references/convert-to-usd/README.md b/skills/omniverse-cad-to-simready/references/convert-to-usd/README.md index e67f2289..b9a1df9f 100644 --- a/skills/omniverse-cad-to-simready/references/convert-to-usd/README.md +++ b/skills/omniverse-cad-to-simready/references/convert-to-usd/README.md @@ -20,8 +20,7 @@ For NVIDIA-backed conversion references, preserve the upstream tool names. The r conversion runs. - CAD routes require the NVIDIA Omniverse `usd-convert-cad` checkout, `omniverse-kit`, supported CAD core extensions, and CAD Converter licensing - on supported architectures. On Linux arm64 only, CAD routes use the - `usd-convert-cad` reference's Kit App Template CAD Converter fallback. + on supported architectures, including Linux arm64. - First CAD runs need network access to download Kit extensions from the NVIDIA registry. @@ -34,7 +33,7 @@ Do not classify NVIDIA-backed inputs from a router-owned extension table. Existi | URDF reference probe | `urdf-usd-converter` | | MuJoCo reference probe, including XML `` root inspection | `mujoco-usd-converter` | | Upstream `usd-convert-gsplat` CLI source inspected by the `usd-convert-gsplat` reference | `usd-convert-gsplat` | -| Upstream `usd-convert-cad` `src/usd_convert_cad/formats.py` inspected by the `usd-convert-cad` reference; on Linux arm64, after dedicated references such as URDF and MuJoCo decline the source, the Kit App Template CAD Converter fallback reports support and lets the installed Kit runtime determine whether conversion can succeed | `usd-convert-cad`; NVIDIA-backed source conversion delegates to upstream `usd-convert-cad` on supported architectures and to the Kit App Template CAD Converter fallback on Linux arm64 only | +| Upstream `usd-convert-cad` SKILL.md Supported Formats table inspected by the `usd-convert-cad` reference probe | `usd-convert-cad`; NVIDIA-backed source conversion delegates to upstream `usd-convert-cad` | | Existing OpenUSD layer or package signature | Skip conversion and route to `validate-usd-minimum` | If more than one converter reference reports support, the router selects by converter-reference priority and records a warning. If no reference reports support, return an unsupported report rather than guessing. @@ -49,15 +48,13 @@ Most conversion routes eventually hand off to a usdex consumer: |---|---| | `urdf-usd-converter` | `urdf_usd_converter`, direct `usdex.core` | | `mujoco-usd-converter` | `mujoco_usd_converter`, direct `usdex.core` | -| `usd-convert-cad` JT route | `omni.kit.converter.jt_core`, linked with `libusdex_core` and `libusdex_rtx` | -| `usd-convert-cad` DGN route | `omni.kit.converter.dgn_core`, linked with `libusdex_core` and `libusdex_rtx` | -| `usd-convert-cad` HOOPS route | `omni.kit.converter.hoops_core`, linked with `libusdex_core` and `libusdex_rtx` | +| `usd-convert-cad` routes | Self-contained `usd-convert-cad==0.2.0` wheel pinned by `upstream-versions.lock.json`, with its bundled OpenUSD and CAD conversion runtime | `usd-convert-asset` is intentionally not an active route until a public PyPI package is available. Downstream validation still follows the OpenUSD Exchange SDK 2.3 stage and metadata contract where applicable; use the OpenUSD Exchange SDK / usdex reference for repo authoring rules and `omni-asset-validate` for -the hub validation wrapper. +the workflow validation wrapper. ## Instructions @@ -66,11 +63,10 @@ the hub validation wrapper. 3. Identify required asset roots such as mesh folders, texture folders, ROS package roots, or MJCF asset directories. 4. Run each converter reference probe and select the first supported reference in router priority order. 5. Confirm the installed reference script and selected converter dependency are available before running conversion. -6. For CAD inputs, let the `usd-convert-cad` reference run upstream - `validate.py` as a delegated readiness gate before it invokes upstream - conversion. On Linux arm64, let that reference start the Kit App Template CAD - Converter fallback. Do not import or start either runtime directly from this - router. +6. For CAD inputs, let the `usd-convert-cad` reference verify the + `usd-convert-cad` wheel is importable in the resolved interpreter before it + invokes conversion. Do not import the wheel or start a converter runtime + directly from this router. 7. Convert to a dedicated output directory with `convert-to-usd`, not next to source files unless the user requested that location. 8. Record a conversion report with source, converter, output, warnings, and next validation step. 9. Hand off the generated USD artifact to `validate-usd-minimum`. @@ -129,10 +125,8 @@ Markdown is acceptable for the first report. JSON can be added later when schema - This router does not own NVIDIA-backed source-extension tables. Update the upstream converter reference or upstream repo when format capability changes. - NVIDIA-backed source conversion must delegate to the `usd-convert-cad` - reference. That reference uses upstream `usd-convert-cad` except for its - Linux arm64 Kit App Template CAD Converter fallback. Do not switch to any - other converter or substitute tooling when the selected runtime is - unavailable. + reference. Do not switch to any other converter or substitute tooling when the + selected runtime is unavailable. - Ambiguous source types are only routed when a converter reference probe reports support. @@ -140,10 +134,19 @@ Markdown is acceptable for the first report. JSON can be added later when schema | Symptom | Cause | Fix | |---------|-------|-----| -| NVIDIA-backed conversion is blocked | `USD_CONVERT_CAD_ROOT`, the upstream checkout, Python 3.12, `omniverse-kit`, the required converter extension, registry access, platform support, or converter licensing is unavailable; on Linux arm64, `KIT_APP_TEMPLATE_ROOT`, the Kit App Template build, Kit executable, or CAD Converter extension is unavailable | Return a `blocked` conversion report with the specific readiness or conversion dependency. Do not switch to another converter or substitute tooling. NVIDIA-backed source conversion must delegate to the `usd-convert-cad` reference. | -| Kit registry access is denied | Upstream `usd-convert-cad validate.py` cannot pull its Kit extensions from the extension registry/CDN | Return the structured `kit_registry_access_denied` diagnostic, including the extension, URL host, exit code, and recovery hint. Fix Horde node egress, proxy, or credentials, or pre-populate/reuse the upstream Kit extension cache, then rerun `OMNI_KIT_ACCEPT_EULA=yes python validate.py` in the upstream checkout. | -| A source routes to an unexpected converter | More than one reference reported support, or an upstream capability source changed | Inspect the report warnings. The router records the priority-selected reference and warns when multiple probes report support. | -| First CAD conversion runs slowly | Kit downloads converter extensions from the registry on first run | Expected on first run only. Subsequent runs use the cached extensions. | +| NVIDIA-backed conversion is blocked | The `usd-convert-cad` wheel is not installed/importable, the resolved interpreter is not Python 3.12, `USD_CONVERT_CAD_PYTHON` points at the wrong interpreter, or converter licensing is unavailable | Return a `blocked` conversion report with the specific readiness or conversion dependency and the install hint. Do not switch to another converter or substitute tooling. NVIDIA-backed source conversion must delegate to the `usd-convert-cad` reference. | +| CAD conversion is blocked on import or version | The pinned `usd-convert-cad==0.2.0` wheel is not installed in the resolved interpreter, a different version is installed, or its bundled `pxr` conflicts with another OpenUSD in a shared interpreter | Install the pinned wheel into an isolated Python 3.12 environment (`python -m pip install usd-convert-cad==0.2.0`) and point `USD_CONVERT_CAD_PYTHON` (or `--usd-convert-cad-python`) at that interpreter, then rerun. | +| A source routes to an unexpected converter | More than one reference reported support, or a capability snapshot changed | Inspect the report warnings. The router records the priority-selected reference and warns when multiple probes report support. | + +## Runtime Availability + +Do not imply that `uv sync` installs every source converter runtime. URDF, +MuJoCo/MJCF, and the repo Python dependencies are handled by the project +environment, but NVIDIA-backed source conversion requires an installed and +validated `NVIDIA-Omniverse/usd-convert-cad` checkout. If that runtime is +missing or does not support the source, preserve the blocked conversion report +and its `install_hint` instead of attempting an unrequested local build or +substituting another converter. ## Unsupported Cases @@ -156,16 +159,14 @@ Do not pretend a conversion succeeded if a converter is unavailable or cannot pa - recommended next action NVIDIA-backed source conversion must delegate to the `usd-convert-cad` -reference, including source types listed by upstream -`src/usd_convert_cad/formats.py`. The upstream route uses converter extensions -such as `omni.kit.converter.hoops_core`, `omni.kit.converter.dgn_core`, and -`omni.kit.converter.jt_core`. On Linux arm64 only, when the router reaches the -`usd-convert-cad` reference after higher-priority dedicated references decline -the source, that CAD reference uses the Kit App Template CAD Converter fallback -and lets the installed Kit runtime decide whether the input can be converted. -If `USD_CONVERT_CAD_ROOT`, the upstream checkout setup, Python 3.12, -`omniverse-kit`, the required converter extension, platform support, licensing, -Kit App Template fallback runtime, or conversion support is unavailable, return -a blocked conversion report rather than switching to any other converter or +reference, including the source types listed in the upstream `usd-convert-cad` +SKILL.md Supported Formats table. The `usd-convert-cad` reference probe parses +that table live from a checkout of the repo (falling back to a built-in +snapshot when offline); conversion itself runs the self-contained +`usd-convert-cad` pip wheel, which bundles its own OpenUSD and CAD conversion +runtime (no Omniverse Kit app or converter extensions). +If the wheel is not importable in the resolved Python 3.12 interpreter, or +platform support, licensing, or conversion support is unavailable, return a +blocked conversion report rather than switching to any other converter or substitute tooling. The higher-level router must not override converter capability with its own source-extension list. diff --git a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/mujoco-usd-converter/README.md b/skills/omniverse-cad-to-simready/references/convert-to-usd/references/mujoco-usd-converter/README.md index 4d1bf746..b7bc64fb 100644 --- a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/mujoco-usd-converter/README.md +++ b/skills/omniverse-cad-to-simready/references/convert-to-usd/references/mujoco-usd-converter/README.md @@ -63,7 +63,7 @@ Prefer the installed reference-local script after confirming dependencies: python3 scripts/run.py /path/to/robot.xml /path/to/usd_robot --report /path/to/conversion_report.json ``` -The wrapper preserves the normalized skill-hub report and forwards supported +The wrapper preserves the normalized omniverse-cad-to-simready report and forwards supported upstream `mujoco_usd_converter` options verbatim. Use the upstream flag names for single-file output, physics-scene omission, verbose conversion, or authored comments: diff --git a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/openusd-exchange-sdk-usdex/README.md b/skills/omniverse-cad-to-simready/references/convert-to-usd/references/openusd-exchange-sdk-usdex/README.md index 45b19eb4..1eafd0e3 100644 --- a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/openusd-exchange-sdk-usdex/README.md +++ b/skills/omniverse-cad-to-simready/references/convert-to-usd/references/openusd-exchange-sdk-usdex/README.md @@ -9,17 +9,17 @@ Exchange README first: For agent-facing authoring guidance, use the upstream USD Exchange skills: `https://github.com/NVIDIA-Omniverse/usd-exchange/tree/main/.agents/skills`. -Do not infer a local explanation of USD Exchange from this Skill Hub reference. +Do not infer a local explanation of USD Exchange from this Omniverse CAD-to-SimReady reference. This repository only records how the upstream USD Exchange SDK / usdex -authoring rules attach to Physical AI Skill Hub code. +authoring rules attach to Omniverse CAD-to-SimReady code. ## When to Use -Use this reference when changing repo code that authors or rewrites USD opinions. It is a Skill-Hub-side binding layer for OpenUSD Exchange SDK 2.3. Keep the upstream SDK skills as the source of authoring rules; this reference only records how those rules attach to this repository. +Use this reference when changing repo code that authors or rewrites USD opinions. It is an Omniverse CAD-to-SimReady binding layer for OpenUSD Exchange SDK 2.3. Keep the upstream SDK skills as the source of authoring rules; this reference only records how those rules attach to this repository. This is a repository-maintenance skill, not a public installed asset-processing runtime. It is documentation-driven and does not ship `scripts/run.py`. -Do not copy the upstream `usd-authoring` reference set into this repo. Use the tagged upstream `v2.3.0` agent skill and the final public package floor `usd-exchange>=2.3.0`. +Do not copy the upstream `usd-authoring` reference set into this repo. Use the tagged upstream `v2.3.0` agent skill and the `usd-exchange==2.3.0` runtime pinned by `upstream-versions.lock.json`. ## When To Apply @@ -41,12 +41,12 @@ Stop when the task is limited to read-only USD validation, packaging manifests, If those URLs are not available, do not substitute stale copied rules. Use an authenticated local checkout of `https://github.com/NVIDIA-Omniverse/usd-exchange` at tag `v2.3.0` and read the same paths from that checkout. -## Skill Hub Bindings +## Omniverse CAD-to-SimReady Bindings Use a module-level `AUTHORING_METADATA` constant in this form: ```text -physical-ai-skill-hub v +omniverse-cad-to-simready v ``` Pass that constant to every `usdex.core.createStage`, `configureStage`, `saveStage`, `saveLayer`, or `exportLayer` call introduced by this repo. @@ -68,7 +68,7 @@ After authoring or conversion, route validation in this order: 3. `omni-asset-validate-geometry` and `omni-asset-validate-physics` as applicable 4. `simready-validate` for SimReady assets -Use the installed `omniverse-cad-to-simready/references/omni-asset-validate/scripts/run.py` wrapper for hub-level validation. Reserve `usdex.test.TestCase.assertIsValidUsd` for unit tests that directly exercise SDK authoring behavior. +Use the installed `omniverse-cad-to-simready/references/omni-asset-validate/scripts/run.py` wrapper for workflow-level validation. Reserve `usdex.test.TestCase.assertIsValidUsd` for unit tests that directly exercise SDK authoring behavior. ## Anti-Pattern Catalog diff --git a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/urdf-usd-converter/README.md b/skills/omniverse-cad-to-simready/references/convert-to-usd/references/urdf-usd-converter/README.md index 5a21713f..be0b2c9f 100644 --- a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/urdf-usd-converter/README.md +++ b/skills/omniverse-cad-to-simready/references/convert-to-usd/references/urdf-usd-converter/README.md @@ -61,7 +61,7 @@ For ROS packages, pass one or more mappings: python3 scripts/run.py /path/to/robot.urdf /path/to/usd_robot --package robot_package=/path/to/assets ``` -The wrapper preserves the normalized skill-hub report and forwards supported +The wrapper preserves the normalized omniverse-cad-to-simready report and forwards supported upstream `urdf_usd_converter` options verbatim. Use the upstream flag names for single-file output, physics-scene omission, or authored comments: diff --git a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-cad/README.md b/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-cad/README.md index 46796e77..3b57e607 100644 --- a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-cad/README.md +++ b/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-cad/README.md @@ -2,114 +2,101 @@ ## When to Use -Use this reference for NVIDIA-backed source conversion. On supported architectures, conversion delegates to upstream `usd-convert-cad`, a headless Omniverse Kit Python wrapper that installs `omniverse-kit`, fetches converter core extensions from the Kit registry, routes supported source formats through its format metadata, and writes its own JSON conversion report. +Use this reference for NVIDIA-backed source conversion. Conversion delegates to the standalone `usd-convert-cad` Python wheel, a self-contained converter that bundles its own OpenUSD (`pxr`) runtime. There is no Omniverse Kit app, no extension-registry pull, no `config.env`, and no EULA prompt. -Guardrail: upstream `usd-convert-cad` is the default converter backend for this reference's NVIDIA-backed source conversion. The only local fallback is Linux arm64, where upstream `usd-convert-cad` is not available yet; that path uses a private NVIDIA Kit App Template application with CAD Converter extensions. Do not fall back to `usd-convert-asset`, hand-authored USD, mesh converters, or other substitute CAD converters. +Guardrail: `usd-convert-cad` is the only allowed converter backend for this reference's NVIDIA-backed source conversion on all supported architectures, including Linux arm64. Do not fall back to `usd-convert-asset`, hand-authored USD, mesh converters, or other substitute CAD converters. ## Upstream Reference - NVIDIA Omniverse `usd-convert-cad` repository: `https://github.com/NVIDIA-Omniverse/usd-convert-cad` -- Upstream CAD conversion skill: `https://github.com/NVIDIA-Omniverse/usd-convert-cad/blob/main/.agents/skills/usd-convert-cad/SKILL.md` -- Linux arm64 fallback: NVIDIA Kit App Template repository: `https://github.com/NVIDIA-Omniverse/kit-app-template` -- Linux arm64 fallback docs: `https://docs.omniverse.nvidia.com/kit/docs/kit-app-template/latest/` -- NVIDIA Omniverse CAD Converter extension docs: `https://docs.omniverse.nvidia.com/kit/docs/omni.kit.converter.cad/latest/` -- NVIDIA HOOPS CAD core extension docs: `https://docs.omniverse.nvidia.com/kit/docs/omni.kit.converter.hoops_core/latest/Overview.html` -- NVIDIA DGN CAD core extension docs: `https://docs.omniverse.nvidia.com/kit/docs/omni.kit.converter.dgn_core/latest/Overview.html` -- NVIDIA JT CAD core extension docs: `https://docs.omniverse.nvidia.com/kit/docs/omni.kit.converter.jt_core/latest/Overview.html` -- Linux arm64 optional service mode docs: `https://docs.omniverse.nvidia.com/kit/docs/omni.services.convert.cad/latest/Usage.html` +- Upstream CAD conversion skill (authoritative Supported Formats and converter options for the tested runtime): `https://github.com/NVIDIA-Omniverse/usd-convert-cad/blob/v0.2.0/skills/omniverse-cad-to-usd/SKILL.md` -Browser, raw-file fetches, or unauthenticated GitHub access can fail depending on access level. If that happens, use an authenticated local clone of `https://github.com/NVIDIA-Omniverse/usd-convert-cad` and read the referenced paths from that checkout. +Preflight clones the ref pinned in `upstream-versions.lock.json` so this SKILL.md is available locally; `run.py --probe` parses that tested Supported Formats table instead of silently adopting changes from a newer upstream release. Browser, raw-file fetches, or unauthenticated GitHub access can fail depending on access level. If that happens, use an authenticated local clone of `https://github.com/NVIDIA-Omniverse/usd-convert-cad` at the pinned ref and read the referenced paths from that checkout. -Use `$HOME/.physical-ai-skill-hub/upstreams/usd-convert-cad` as the default stable upstream checkout path. Set `PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT` to move the shared upstream root, or set `USD_CONVERT_CAD_ROOT` / `--usd-convert-cad-root` for this converter only. An existing legacy `$HOME/.usd-convert-cad` checkout is still accepted when no shared root is configured, but new setup should use the shared upstream root. Do not use `/tmp` as the runtime checkout location for conversions. A `/tmp` clone is acceptable only for short-lived inspection. - -On Linux arm64 only, the wrapper does not require `USD_CONVERT_CAD_ROOT`. Instead it locates Kit App Template from `KIT_APP_TEMPLATE_ROOT` or `$HOME/.kit-app-template`, the build directory from `KIT_APP_TEMPLATE_BUILD_DIR` or `_build//release`, and the Kit executable from `KIT_APP_TEMPLATE_KIT_EXECUTABLE`, `KIT_EXECUTABLE`, or `/kit/kit`. +The wheel bundles its own OpenUSD (`pxr`) runtime, so install it into a dedicated Python 3.12 interpreter to avoid clashing with any other `pxr` distribution in a shared environment. Point this reference at that interpreter with `--usd-convert-cad-python` or the `USD_CONVERT_CAD_PYTHON` environment variable. When neither is set, the reference reads the interpreter from the preflight manifest (`usd_convert_cad` runtime), then falls back to the interpreter running the script. ## Inputs -Collect a source file, output directory, and optional `--usd-convert-cad-root`. -Supported source suffixes and converter routing belong to upstream -`SUPPORTED_FORMATS` / format metadata in `src/usd_convert_cad/formats.py` and -`.agents/skills/usd-convert-cad/SKILL.md`. -This reference reads the upstream formats table from the configured checkout -when it is available and keeps only a fallback snapshot for blocked reports when -the checkout is missing. Examples such as `.stp`, `.step`, `.igs`, `.iges`, -`.dgn`, `.ifc`, `.ifczip`, `.jt`, and proprietary CAD files route to -`usd-convert-cad`, never to a substitute converter. Mesh/scene formats also -route here when upstream `usd-convert-cad` lists them as supported; otherwise -they are reported unsupported rather than sent to `usd-convert-asset`. -Do not choose `jt_core`, `dgn_core`, or `hoops_core` in this wrapper; upstream -`usd-convert-cad` selects the converter from its supported-format metadata. -Legacy backend-selection arguments are accepted for compatibility, but the value -is ignored by this wrapper and is never forwarded to upstream `convert.py`. - -On Linux arm64, probe support does not use a local source-format allowlist -because the Kit App Template fallback is the scoped architecture workaround. -The fallback reports support and lets the installed Kit CAD Converter runtime -determine whether the input can be converted. The fallback accepts Kit App -Template options such as `--kit-app-template-root`, -`--kit-build-dir`, `--kit-executable`, `--execution-mode`, `--config-path`, -`--fine`, and `--coarse`; these options are ignored by the upstream path on -other architectures. +Collect a source file, an output directory, and optionally the converter interpreter via `--usd-convert-cad-python` (or `USD_CONVERT_CAD_PYTHON`). +Supported source suffixes come from the upstream Supported Formats table in the +`usd-convert-cad` SKILL.md (`skills/omniverse-cad-to-usd/SKILL.md`). Because the +wheel does not expose a machine-readable format registry, `run.py` reads that +table directly from a checkout of the repo and resolves the SKILL.md in this +order: + +1. `--usd-convert-cad-skill ` +2. `USD_CONVERT_CAD_SKILL` (SKILL.md file or a checkout directory) +3. `--usd-convert-cad-root ` +4. the preflight manifest's `usd_convert_cad` upstream checkout +5. `USD_CONVERT_CAD_ROOT` +6. the shared upstream checkout (`OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT/usd-convert-cad`) + +When none of these is reachable (for example, a fully offline run), `run.py` +falls back to a built-in snapshot of the table (`SUPPORTED_CAD_SUFFIXES`), so +conversion is never blocked purely by a missing checkout. Examples such as +`.stp`, `.step`, `.igs`, `.iges`, `.dgn`, `.ifc`, `.ifczip`, `.jt`, and +proprietary CAD files route to `usd-convert-cad`, never to a substitute +converter. Mesh/scene formats also route here when the table lists them as +supported; otherwise they are reported unsupported rather than sent to +`usd-convert-asset`. +The legacy backend-selection argument (`--backend`) is still accepted for +compatibility but ignored; the wheel selects its converter internally. +`--usd-convert-cad-root` now selects the SKILL.md checkout (see above) instead of +a conversion runtime. ## Dependency Check Require: -- `usd-convert-cad` from this repo. -- A local `NVIDIA-Omniverse/usd-convert-cad` checkout from `https://github.com/NVIDIA-Omniverse/usd-convert-cad`, preferably at `$HOME/.physical-ai-skill-hub/upstreams/usd-convert-cad`. -- Python 3.12 available for upstream setup. -- Upstream setup completed with the upstream runtime Python, for example - `.venv/bin/python install.py` from the upstream checkout after the venv is - created. -- Upstream environment validated with `python validate.py`. -- Network access to the Kit extension registry on first run. -- Accepted Omniverse terms for non-interactive runs. The wrappers set or expect `OMNI_KIT_ACCEPT_EULA=yes`. - -For Linux arm64 fallback, require: +- The manifest-pinned `usd-convert-cad==0.2.0` wheel installed into a Python 3.12 environment + (`python -m pip install usd-convert-cad==0.2.0`). If the wheel is hosted on an NVIDIA + package index, add it, for example `--extra-index-url https://pypi.nvidia.com`. +- An isolated interpreter for that wheel (its bundled `pxr` can conflict with + another OpenUSD in a shared interpreter), exposed via `--usd-convert-cad-python`, + `USD_CONVERT_CAD_PYTHON`, or the preflight manifest. -- Local `NVIDIA-Omniverse/kit-app-template` checkout from `https://github.com/NVIDIA-Omniverse/kit-app-template`, preferably at `$HOME/.kit-app-template`. -- A built Kit App Template app whose dependencies include `omni.kit.converter.cad` or the specific CAD core extension required by the input: `omni.kit.converter.hoops_core`, `omni.kit.converter.dgn_core`, or `omni.kit.converter.jt_core`. -- Built Kit executable under `_build//release/kit/kit` or an explicit `--kit-executable`. -- For optional `--execution-mode service`, `omni.services.convert.cad-*` in `_build//release/extscache`, including `omni/services/convert/cad/services/process/{hoops,dgn,jt}_main.py`. -- Accepted Omniverse terms for non-interactive runs. The fallback sets `ACCEPT_EULA=Y` and `OMNI_KIT_ACCEPT_EULA=yes`. - -Do not silently install or build missing dependencies. If the checkout, `.venv`, `omniverse-kit`, converter core extension, platform support, Kit App Template build, or CAD Converter license is unavailable, run the wrapper and preserve its blocked conversion report. This reference may invoke upstream `validate.py` to verify readiness on supported architectures. On Linux arm64 it may start the local Kit App Template runtime because that is the scoped fallback path. +Do not silently install missing dependencies. If the wheel is not importable in +the resolved interpreter, run the wrapper and preserve its blocked conversion +report with the install hint. `check_dependencies.py` verifies the wheel imports +(`import usd_convert_cad; print(usd_convert_cad.get_version())`) and is the CAD +readiness gate before batching per-asset conversions. ## Conversion Workflow 1. Confirm the source asset exists. -2. On Linux arm64, when this CAD reference is selected by the higher-level router, use the Kit App Template CAD Converter fallback and let the installed Kit runtime determine conversion support. -3. On other architectures, confirm upstream `usd-convert-cad` lists the source suffix as supported. -4. Locate the upstream checkout from `--usd-convert-cad-root`, `USD_CONVERT_CAD_ROOT`, `$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/usd-convert-cad`, or `$HOME/.physical-ai-skill-hub/upstreams/usd-convert-cad`; on Linux arm64, locate Kit App Template from `KIT_APP_TEMPLATE_ROOT`, `KIT_APP_TEMPLATE_BUILD_DIR`, or `KIT_APP_TEMPLATE_KIT_EXECUTABLE`. -5. If setup state is unknown, follow the selected runtime's install/validate guidance. -6. Run this installed reference's portable script; before upstream conversion it delegates readiness to upstream `validate.py`, then calls upstream `python "$USD_CONVERT_CAD_ROOT/convert.py" ... --quiet --report ...`. On Linux arm64, it starts the built Kit executable with the selected CAD core extension and a generated runner/config sidecar. -7. Preserve both reports on the upstream path: this repo's normalized conversion report and the upstream `*_usd_convert_cad_status.json` sidecar. Preserve generated Kit fallback sidecars on the Linux arm64 path. -8. If USD is generated, hand it to `validate-usd-minimum`. -9. If blocked, report the exact upstream readiness or fallback runtime failure, such as a missing checkout, stale setup, Python 3.12 issue, `omniverse-kit` issue, missing Kit App Template build, required CAD core extension, platform issue, registry download failure, conversion failure, or CAD license dependency. +2. Confirm the source suffix appears in the `usd-convert-cad` SKILL.md Supported Formats table (parsed live from the resolved checkout, with the built-in snapshot as fallback). + On Linux aarch64, reject `.dwg`, `.dxf`, `.rvt`, and `.rfa` before invoking + the wheel because the upstream DWG/Revit readers are unavailable there. +3. Resolve the converter interpreter from `--usd-convert-cad-python`, `USD_CONVERT_CAD_PYTHON`, or the preflight manifest. +4. If the wheel is not installed or does not match `upstream-versions.lock.json`, follow the install hint (`python -m pip install usd-convert-cad==0.2.0` into an isolated Python 3.12 environment). +5. Run this reference's portable script. It invokes `python -m usd_convert_cad -i -o /.usd` with the resolved interpreter. +6. Treat the converter exit code as authoritative: exit `0` with the expected USD present is success; non-zero or a missing output is a failure with the converter's stderr recorded in the report. +7. If USD is generated, hand it to `validate-usd-minimum`. +8. If blocked, report the exact failure, such as an uninstalled or unimportable wheel, a wrong Python version, an unsupported source format, a conversion error, or a CAD license dependency. ## CLI Pattern -Default STEP conversion: +Default STEP conversion (uses `USD_CONVERT_CAD_PYTHON` or the current interpreter): ```bash python3 scripts/run.py asset.step output_dir \ --report output_dir/conversion.json ``` -Explicit upstream checkout: +Explicit converter interpreter: ```bash python3 scripts/run.py asset.jt output_dir \ - --usd-convert-cad-root /path/to/usd-convert-cad \ + --usd-convert-cad-python /path/to/.venv/bin/python \ --report output_dir/conversion.json ``` -Linux arm64 fallback with explicit Kit App Template build: +Forward documented converter options straight through to the wheel: ```bash -python3 scripts/run.py asset.step output_dir \ - --kit-build-dir /path/to/kit-app-template/_build/linux-aarch64/release \ - --report output_dir/conversion.json +python3 scripts/run.py asset.jt output_dir \ + --report output_dir/conversion.json \ + --tess-lod 4 --no-materials ``` When running from outside the reference directory, use the installed reference path: @@ -124,37 +111,32 @@ Check dependencies with: python3 scripts/check_dependencies.py --report dependency-check.json ``` -The dependency check delegates to upstream `validate.py` when the checkout, -`convert.py`, and `validate.py` are present. It can start the upstream runtime -and access the extension registry; use it as the CAD readiness gate before -batching per-asset conversions. On Linux arm64, the dependency check reports -Kit App Template root, build directory, and Kit executable readiness instead. - ## Output Format -This repo normalizes the upstream status into the shared conversion report contract and includes: +This repo normalizes the conversion into the shared conversion report contract and includes: - `source_asset_path` - `source_format: cad` - `converter_skill: usd-convert-cad` -- `converter_tool: usd-convert-cad` on the upstream path, or `NVIDIA Kit App Template CAD Converter` on Linux arm64 fallback -- `converter_command`, the upstream `convert.py` invocation with explicit output USD and report paths, or the Kit executable command that enables the selected CAD core fallback extension +- `converter_tool: usd-convert-cad` +- `converter_command`, the `python -m usd_convert_cad -i -o ` invocation (plus any forwarded converter options) - `output_directory` - `output_usd_path` - `generated_files` -- `sidecar_inputs`, including the upstream checkout, upstream JSON report, and upstream log when available, or the Kit App Template checkout/build/config/runner sidecars for Linux arm64 fallback -- `warnings`, including the selected runtime and converter core +- `sidecar_inputs`, including the resolved converter interpreter +- `warnings`, including the resolved interpreter and installed wheel version - `errors` - `next_step: validate-usd-minimum` -The upstream sidecar report includes the selected converter extension, converter module, converter options, elapsed time, pass/fail status, and upstream warnings/errors. +The wheel writes only the requested USD output (and any sidecars the chosen USD format implies); it does not emit a separate JSON status report. ## Known Caveats -- Upstream `usd-convert-cad` is still Omniverse Kit and CAD Converter based; the Linux arm64 fallback uses Kit App Template and CAD Converter directly until upstream supports that architecture. -- Python 3.12 is required by upstream setup. -- The first conversion can take longer because Kit downloads converter extensions from the registry. -- If `validate.py` reports `Result.ERROR_ACCESS_DENIED` while pulling a Kit extension, treat it as an upstream Kit registry/CDN access problem, not a routing problem. The portable scripts report `kind: kit_registry_access_denied` with the extension, URL host, exit code, and a recovery hint. Fix Horde node egress, proxy, or credentials, or pre-populate and reuse the upstream Kit extension cache, then rerun `OMNI_KIT_ACCEPT_EULA=yes python validate.py`. +- The `usd-convert-cad` wheel bundles its own OpenUSD and CAD conversion runtime; install it into an isolated Python 3.12 environment so its `pxr` cannot conflict with another OpenUSD distribution. +- Python 3.12 is required by the wheel. +- On Linux aarch64, AutoCAD `.dwg`/`.dxf` and Revit `.rvt`/`.rfa` inputs are + unsupported by the upstream wheel and are reported as a platform limitation. +- USDZ (`.usdz`) export is not supported yet; use `.usd`, `.usda`, or `.usdc`. - Proprietary CAD formats can require CAD Converter licensing. -- Detailed converter option names must come from the upstream skill, Kit App Template docs, or installed extension docs. +- Detailed converter option names must come from the upstream skill or `usd-convert-cad --help` for the installed version. - A successful CAD conversion does not imply simulation readiness. diff --git a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-cad/scripts/check_dependencies.py b/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-cad/scripts/check_dependencies.py index 6409fcb3..55e894e7 100644 --- a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-cad/scripts/check_dependencies.py +++ b/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-cad/scripts/check_dependencies.py @@ -5,58 +5,24 @@ from __future__ import annotations import argparse -import json -import os from pathlib import Path -import subprocess import sys from typing import Any sys.path.insert(0, str(Path(__file__).resolve().parents[5] / "shared")) -import kit_app_template_cad -from preflight_manifest import load_preflight_manifest, preflight_required, preflight_status_check, ready_path_from_runtime -from script_utils import check_result as _check, emit_json_report, subprocess_output, tail_text -from usd_convert_cad_diagnostics import summarize_usd_convert_cad_validation_failure - - -SKILL = "usd-convert-cad" -UPSTREAM_REPO_URL = "https://github.com/NVIDIA-Omniverse/usd-convert-cad" -UPSTREAM_ROOT_ENV = "PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT" -INSTALL_HINT = ( - 'export PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT="${PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT:-$HOME/.physical-ai-skill-hub/upstreams}" ' - "&& mkdir -p \"$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT\" " - f"&& git clone {UPSTREAM_REPO_URL} \"$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/usd-convert-cad\" " - "&& cd \"$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/usd-convert-cad\" " - "&& OMNI_KIT_ACCEPT_EULA=yes python install.py && python validate.py" +from preflight_manifest import preflight_required, preflight_status_check +from script_utils import check_result as _check, emit_json_report + +from run import ( + CONVERTER_PYTHON_ENV, + EXPECTED_WHEEL_VERSION, + INSTALL_HINT, + SKILL, + UPSTREAM_REPO_URL, + resolve_converter_python, + wheel_version, ) -UPSTREAM_PREFLIGHT_TIMEOUT_SECONDS = 600 - - -def default_upstream_root() -> Path: - root = os.environ.get(UPSTREAM_ROOT_ENV) - if root: - return Path(root).expanduser() / "usd-convert-cad" - return Path.home() / ".physical-ai-skill-hub" / "upstreams" / "usd-convert-cad" - - -def resolve_usd_convert_cad_root(explicit: Path | None) -> Path: - if explicit is not None: - return explicit.expanduser().resolve() - manifest, _, _ = load_preflight_manifest() - manifest_root = ready_path_from_runtime(manifest, "usd_convert_cad") - if manifest_root is not None: - return manifest_root - env_root = os.environ.get("USD_CONVERT_CAD_ROOT") - if env_root: - return Path(env_root).expanduser().resolve() - default = default_upstream_root().expanduser() - if default.exists(): - return default.resolve() - legacy = Path("~/.usd-convert-cad").expanduser() - if legacy.exists(): - return legacy.resolve() - return default.resolve() def _write_report(payload: dict[str, Any], report_path: Path | None) -> None: @@ -64,23 +30,9 @@ def _write_report(payload: dict[str, Any], report_path: Path | None) -> None: def check_dependencies( - usd_convert_cad_root: Path | None = None, - *, - kit_app_template_root: Path | None = None, - kit_build_dir: Path | None = None, - kit_executable: Path | None = None, - cad_service_extension_dir: Path | None = None, - execution_mode: str = "core", + converter_python: Path | None = None, ) -> dict[str, Any]: - if kit_app_template_cad.is_arm64_host(): - return kit_app_template_cad.check_dependencies( - kit_app_template_root=kit_app_template_root, - kit_build_dir=kit_build_dir, - kit_executable=kit_executable, - cad_service_extension_dir=cad_service_extension_dir, - execution_mode=execution_mode, - ) - if preflight_required() and usd_convert_cad_root is None: + if preflight_required() and converter_python is None: preflight_check = preflight_status_check("usd-convert-cad", "usd_convert_cad") if not preflight_check["passed"]: return { @@ -90,116 +42,62 @@ def check_dependencies( "errors": [preflight_check["message"]], "install_hint": preflight_check["message"], } - upstream_root = resolve_usd_convert_cad_root(usd_convert_cad_root) + + python = resolve_converter_python(converter_python) + version, version_error = wheel_version(python) + version_matches = version == EXPECTED_WHEEL_VERSION checks = [ - _check("python_available", True, f"Python executable: {sys.executable}"), - _check("usd_convert_cad_root_exists", upstream_root.exists(), f"usd-convert-cad root: {upstream_root}"), - _check("usd_convert_cad_convert_py_exists", (upstream_root / "convert.py").exists(), f"convert.py under: {upstream_root}"), - _check("usd_convert_cad_validate_py_exists", (upstream_root / "validate.py").exists(), f"validate.py under: {upstream_root}"), + _check("orchestrator_python_available", True, f"Orchestrator Python: {sys.executable}"), + _check( + "usd_convert_cad_python_resolved", + python.exists(), + f"usd-convert-cad interpreter: {python}" + + ("" if python.exists() else " (path does not exist)"), + ), + _check( + "usd_convert_cad_wheel_importable", + version is not None, + f"usd-convert-cad wheel version: {version}" + if version is not None + else f"usd-convert-cad wheel is not importable with {python}: {version_error}", + ), + _check( + "usd_convert_cad_wheel_version", + version_matches, + f"usd-convert-cad requires version {EXPECTED_WHEEL_VERSION}, found {version or 'not installed'}", + ), ] - if all(check["passed"] for check in checks): - checks.append(check_upstream_validation(upstream_root)) errors = [check["message"] for check in checks if not check["passed"]] - diagnostics = [ - diagnostic - for check in checks - for diagnostic in check.get("diagnostics", []) - if isinstance(diagnostic, dict) - ] payload = { "skill": SKILL, "passed": not errors, - "upstream_root": str(upstream_root), + "converter_python": str(python), + "converter_python_env": CONVERTER_PYTHON_ENV, "upstream_repo": UPSTREAM_REPO_URL, + "wheel_version": version, "checks": checks, "errors": errors, } - if diagnostics: - payload["diagnostics"] = diagnostics if errors: payload["install_hint"] = INSTALL_HINT return payload -def check_upstream_validation(upstream_root: Path) -> dict[str, Any]: - command = [sys.executable, str(upstream_root / "validate.py")] - env = os.environ.copy() - env.setdefault("OMNI_KIT_ACCEPT_EULA", "yes") - try: - completed = subprocess.run( - command, - cwd=str(upstream_root), - env=env, - capture_output=True, - text=True, - timeout=UPSTREAM_PREFLIGHT_TIMEOUT_SECONDS, - check=False, - ) - except subprocess.TimeoutExpired as exc: - output = subprocess_output(getattr(exc, "stdout", ""), getattr(exc, "stderr", "")) - detail = tail_text(output) - message = ( - "upstream usd-convert-cad readiness validation timed out after " - f"{UPSTREAM_PREFLIGHT_TIMEOUT_SECONDS}s. Resolve the upstream usd-convert-cad runtime and rerun validate.py." - ) - if detail: - message = f"{message} Output: {detail}" - return _check("usd_convert_cad_upstream_validate_passes", False, message) - - output = subprocess_output(completed.stdout, completed.stderr) - if completed.returncode == 0: - return _check( - "usd_convert_cad_upstream_validate_passes", - True, - f"upstream validate.py passed using command: {' '.join(command)}", - ) - - detail = tail_text(output) or f"validate.py exited with {completed.returncode}" - message = ( - "upstream usd-convert-cad readiness validation failed " - f"(exit {completed.returncode}): {detail}. Resolve the upstream usd-convert-cad runtime and rerun validate.py." - ) - diagnostic = summarize_usd_convert_cad_validation_failure(output, completed.returncode) - if diagnostic: - message = ( - "upstream usd-convert-cad readiness validation failed " - f"(exit {completed.returncode}): {diagnostic['summary']} " - f"{diagnostic['recovery_hint']} Output: {detail}" - ) - check = _check( - "usd_convert_cad_upstream_validate_passes", - False, - message, - ) - if diagnostic: - check["diagnostics"] = [diagnostic] - return check - - def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(description="Check portable usd-convert-cad dependencies.") - parser.add_argument("--usd-convert-cad-root", type=Path) - parser.add_argument("--kit-app-template-root", type=Path, help="Linux arm64 fallback: local Kit App Template checkout path.") - parser.add_argument("--kit-build-dir", type=Path, help="Linux arm64 fallback: built Kit App Template _build//release directory.") - parser.add_argument("--kit-executable", type=Path, help="Linux arm64 fallback: built Kit executable.") - parser.add_argument("--cad-service-extension-dir", type=Path, help="Linux arm64 fallback service mode: omni.services.convert.cad extension directory.") + parser = argparse.ArgumentParser(description="Check usd-convert-cad wheel readiness.") parser.add_argument( - "--execution-mode", - default="core", - choices=["core", "service"], - help="Linux arm64 fallback: dependency checks for direct CAD core mode or CAD service mode.", + "--usd-convert-cad-python", + type=Path, + help=( + "Python interpreter that has the usd-convert-cad wheel installed. " + f"Defaults to {CONVERTER_PYTHON_ENV}, the preflight manifest, then this interpreter." + ), ) + parser.add_argument("--usd-convert-cad-root", type=Path, help=argparse.SUPPRESS) parser.add_argument("--report", type=Path, help="Write dependency check JSON to this path.") args = parser.parse_args(argv) - payload = check_dependencies( - args.usd_convert_cad_root, - kit_app_template_root=args.kit_app_template_root, - kit_build_dir=args.kit_build_dir, - kit_executable=args.kit_executable, - cad_service_extension_dir=args.cad_service_extension_dir, - execution_mode=args.execution_mode, - ) + payload = check_dependencies(args.usd_convert_cad_python) _write_report(payload, args.report) return 0 if payload["passed"] else 1 diff --git a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-cad/scripts/kit_app_template_cad.py b/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-cad/scripts/kit_app_template_cad.py deleted file mode 100644 index ab702bb2..00000000 --- a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-cad/scripts/kit_app_template_cad.py +++ /dev/null @@ -1,775 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import json -import os -from pathlib import Path -import platform -import shlex -import subprocess -import sys -from typing import Any - -sys.path.insert(0, str(Path(__file__).resolve().parents[5] / "shared")) - -from script_utils import check_result as _check - - -SKILL = "usd-convert-cad" -NEXT_STEP = "validate-usd-minimum" -KIT_APP_TEMPLATE_URL = "https://github.com/NVIDIA-Omniverse/kit-app-template" -KIT_APP_TEMPLATE_DOCS_URL = "https://docs.omniverse.nvidia.com/kit/docs/kit-app-template/latest/" -CAD_CONVERTER_DOCS_URL = "https://docs.omniverse.nvidia.com/kit/docs/omni.kit.converter.cad/latest/" -CAD_CONVERTER_SERVICE_DOCS_URL = "https://docs.omniverse.nvidia.com/kit/docs/omni.services.convert.cad/latest/Usage.html" -DEFAULT_KIT_APP_TEMPLATE_ROOT = Path("~/.kit-app-template") -KIT_CAD_CONVERTER_TOOL = "NVIDIA Kit App Template CAD Converter" -KIT_INSTALL_HINT = ( - "Clone https://github.com/NVIDIA-Omniverse/kit-app-template to $HOME/.kit-app-template, " - "add the required CAD Converter extension to the app dependencies, then run " - "`./repo.sh template new` and `./repo.sh build --config release`. Set " - "KIT_APP_TEMPLATE_ROOT, KIT_APP_TEMPLATE_BUILD_DIR, or KIT_APP_TEMPLATE_KIT_EXECUTABLE " - "when using a non-default location." -) -USD_OUTPUT_SUFFIXES = {".usd", ".usda", ".usdc", ".usdz"} -CAD_CORE_BY_SUFFIX = { - ".dgn": "dgn", - ".jt": "jt", -} -CAD_CORE_EXTENSION = { - "dgn": "omni.kit.converter.dgn_core", - "hoops": "omni.kit.converter.hoops_core", - "jt": "omni.kit.converter.jt_core", -} -CAD_PROCESS_SCRIPT = { - "dgn": "dgn_main.py", - "hoops": "hoops_main.py", - "jt": "jt_main.py", -} -CAD_CORE_MODULE = { - "dgn": "omni.kit.converter.dgn_core", - "hoops": "omni.kit.converter.hoops_core", - "jt": "omni.kit.converter.jt_core", -} -FINE_TESSELLATION_CHORD = 0.001 -FINE_TESSELLATION_ANGLE = 10.0 -COARSE_TESSELLATION_CHORD = 0.1 -COARSE_TESSELLATION_ANGLE = 45.0 - - -def is_arm64_host() -> bool: - return platform.machine().lower() in {"aarch64", "arm64"} - - -def real_suffix(source_asset: Path) -> str: - suffix = source_asset.suffix.lower() - if suffix.lstrip(".").isdigit(): - return Path(source_asset.stem).suffix.lower() - return suffix - - -def kit_supports_source(source_asset: Path) -> bool: - return True - - -def discover_generated_files(output_directory: Path) -> list[str]: - if not output_directory.exists(): - return [] - return sorted( - str(path.relative_to(output_directory)) - for path in output_directory.rglob("*") - if path.is_file() - ) - - -def _compact(value: str, limit: int = 4000) -> str: - value = value.strip() - if len(value) <= limit: - return value - return value[:limit] + "\n... truncated ..." - - -def _host_platform() -> str: - system = platform.system().lower() - machine = platform.machine().lower() - if machine in {"amd64", "x86_64"}: - arch = "x86_64" - elif machine in {"arm64", "aarch64"}: - arch = "aarch64" - else: - arch = machine - if system == "windows": - return f"windows-{arch}" - if system == "linux": - return f"linux-{arch}" - return f"{system}-{arch}" - - -def _resolve_path(value: str | Path | None, env_name: str) -> Path | None: - if value: - return Path(value).expanduser().resolve() - env_value = os.getenv(env_name) - if env_value: - return Path(env_value).expanduser().resolve() - return None - - -def _resolve_kit_root(kit_app_template_root: Path | None) -> Path: - explicit = _resolve_path(kit_app_template_root, "KIT_APP_TEMPLATE_ROOT") - if explicit is not None: - return explicit - return DEFAULT_KIT_APP_TEMPLATE_ROOT.expanduser().resolve() - - -def _resolve_build_dir(kit_build_dir: Path | None, kit_root: Path | None) -> Path | None: - explicit = _resolve_path(kit_build_dir, "KIT_APP_TEMPLATE_BUILD_DIR") - if explicit is not None: - return explicit - if kit_root is not None: - return (kit_root / "_build" / _host_platform() / "release").resolve() - return None - - -def _resolve_kit_executable(kit_executable: Path | None, kit_build_dir: Path | None) -> Path | None: - explicit = _resolve_path(kit_executable, "KIT_APP_TEMPLATE_KIT_EXECUTABLE") - if explicit is not None: - return explicit - explicit = _resolve_path(None, "KIT_EXECUTABLE") - if explicit is not None: - return explicit - if kit_build_dir is None: - return None - name = "kit.exe" if platform.system().lower() == "windows" else "kit" - return (kit_build_dir / "kit" / name).resolve() - - -def _resolve_service_extension_dir( - cad_service_extension_dir: Path | None, - kit_build_dir: Path | None, -) -> Path | None: - explicit = _resolve_path(cad_service_extension_dir, "KIT_CAD_SERVICE_EXTENSION_DIR") - if explicit is not None: - return explicit - if kit_build_dir is None: - return None - extension_cache = kit_build_dir / "extscache" - if not extension_cache.exists(): - return None - candidates = sorted( - extension_cache.glob("omni.services.convert.cad-*"), - key=lambda path: path.stat().st_mtime if path.exists() else 0, - reverse=True, - ) - return candidates[0].resolve() if candidates else None - - -def _cad_core_for_source(source_asset: Path) -> str: - return CAD_CORE_BY_SUFFIX.get(real_suffix(source_asset), "hoops") - - -def _process_script_path(service_extension_dir: Path | None, cad_core: str) -> Path | None: - if service_extension_dir is None: - return None - return ( - service_extension_dir - / "omni" - / "services" - / "convert" - / "cad" - / "services" - / "process" - / CAD_PROCESS_SCRIPT[cad_core] - ).resolve() - - -def _quality_options( - *, - fine: bool, - coarse: bool, - tessellation_chord: float, - tessellation_angle: float, -) -> tuple[float, float]: - if fine: - return FINE_TESSELLATION_CHORD, FINE_TESSELLATION_ANGLE - if coarse: - return COARSE_TESSELLATION_CHORD, COARSE_TESSELLATION_ANGLE - return tessellation_chord, tessellation_angle - - -def _write_converter_config( - output_directory: Path, - source_asset: Path, - *, - fine: bool, - coarse: bool, - tessellation_chord: float, - tessellation_angle: float, - no_materials: bool, - single_mesh: bool, - no_meter_units: bool, - keep_hidden: bool, -) -> Path: - chord, angle = _quality_options( - fine=fine, - coarse=coarse, - tessellation_chord=tessellation_chord, - tessellation_angle=tessellation_angle, - ) - options = { - "instancing": True, - "bOptimize": True, - "convertHidden": keep_hidden, - "dMetersPerUnit": 0.0 if no_meter_units else 1.0, - "iUpAxis": 2, - "dChordHeight": chord, - "dAngleTolerance": angle, - "importMaterials": not no_materials, - "singleMesh": single_mesh, - } - config_path = output_directory / f"{source_asset.stem}_cad_converter_options.json" - config_path.write_text(json.dumps(options, indent=2, sort_keys=True) + "\n", encoding="utf-8") - return config_path - - -def _write_core_runner_script(output_directory: Path, source_asset: Path) -> Path: - runner_path = output_directory / f"{source_asset.stem}_kit_cad_core_runner.py" - runner_path.write_text( - """from __future__ import annotations - -import argparse -import asyncio -import importlib -import inspect -import json -from pathlib import Path -import sys -import time - - -def _string_options(options: dict) -> dict[str, str]: - result = {} - for key, value in options.items(): - if isinstance(value, bool): - result[key] = "true" if value else "false" - else: - result[key] = str(value) - return result - - -def _run_async(awaitable): - try: - loop = asyncio.get_event_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - return loop.run_until_complete(awaitable) - - -def _status_code(status): - for name in ("error_code", "code", "status", "result"): - if hasattr(status, name): - return getattr(status, name) - if isinstance(status, (tuple, list)) and status: - return status[0] - return None - - -def _status_text(status) -> str: - for name in ("error_message", "message", "details"): - if hasattr(status, name): - return str(getattr(status, name)) - if isinstance(status, (tuple, list)) and len(status) >= 2: - return str(status[1]) - return str(status) - - -def _result_success_and_message(result) -> tuple[bool, str]: - if isinstance(result, tuple) and len(result) >= 2: - primary, secondary = result[0], result[1] - if isinstance(primary, bool): - success = primary - elif isinstance(primary, int): - success = primary == 0 - elif isinstance(primary, str): - success = bool(primary) - else: - success = bool(primary) - - if not isinstance(secondary, str): - code = _status_code(secondary) - if isinstance(code, bool): - success = success and code - elif isinstance(code, int): - success = success and code == 0 - return bool(success), _status_text(secondary) - return bool(result), str(result) - - -def _wait_for_instance(module): - converter = module.get_instance() - if converter is not None: - return converter - try: - import omni.kit.app - - app = omni.kit.app.get_app() - except Exception: - app = None - for _ in range(60): - if app is not None: - app.update() - time.sleep(0.5) - converter = module.get_instance() - if converter is not None: - return converter - return None - - -def _convert_with_core_instance(module_name: str, input_path: Path, output_path: Path, options: dict) -> tuple[bool, str]: - module = importlib.import_module(module_name) - converter = _wait_for_instance(module) - if converter is None: - return False, f"{module_name}.get_instance() returned None" - string_options = _string_options(options) - try: - task = converter.create_converter_task(str(input_path), str(output_path), options) - except TypeError: - task = converter.create_converter_task(str(input_path), str(output_path), string_options) - if hasattr(task, "wait_until_finished"): - success = _run_async(task.wait_until_finished()) - status = getattr(task, "get_status", lambda: "")() - return bool(success), str(status) - if inspect.isawaitable(task): - result = _run_async(task) - return _result_success_and_message(result) - return _result_success_and_message(task) - - -def _convert_with_hoops_function(input_path: Path, output_path: Path, options: dict) -> tuple[bool, str]: - import omni.converter.hoops as hoops - - params = hoops.Parameters() - string_options = _string_options(options) - if hasattr(params, "parseArgs"): - params.parseArgs(string_options) - elif hasattr(params, "parse"): - params.parse(string_options) - result = hoops.convert(params, str(input_path), str(output_path), string_options) - return _result_success_and_message(result) - - -def main() -> int: - parser = argparse.ArgumentParser(description="Run direct Kit CAD core conversion.") - parser.add_argument("--core-module", required=True) - parser.add_argument("--input-path", required=True, type=Path) - parser.add_argument("--output-path", required=True, type=Path) - parser.add_argument("--config-path", required=True, type=Path) - args = parser.parse_args() - - options = json.loads(args.config_path.read_text(encoding="utf-8")) - input_path = args.input_path.resolve() - output_path = args.output_path.resolve() - output_path.parent.mkdir(parents=True, exist_ok=True) - - if args.core_module == "omni.kit.converter.hoops_core": - try: - success, message = _convert_with_hoops_function(input_path, output_path, options) - except Exception as first_error: - success, message = _convert_with_core_instance(args.core_module, input_path, output_path, options) - if not success: - message = f"hoops direct function failed: {first_error}; core instance failed: {message}" - else: - success, message = _convert_with_core_instance(args.core_module, input_path, output_path, options) - - if not success: - print(message, file=sys.stderr) - return 1 - if not output_path.exists(): - print(f"converter reported success but did not write {output_path}", file=sys.stderr) - return 1 - print(message) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) -""", - encoding="utf-8", - ) - return runner_path - - -def _kit_exec_payload(process_script: Path, source_asset: Path, output_usd_path: Path, config_path: Path) -> str: - return " ".join( - [ - shlex.quote(str(process_script)), - "--input-path", - shlex.quote(str(source_asset)), - "--output-path", - shlex.quote(str(output_usd_path)), - "--config-path", - shlex.quote(str(config_path)), - ] - ) - - -def _core_exec_payload(runner_script: Path, cad_core: str, source_asset: Path, output_usd_path: Path, config_path: Path) -> str: - return " ".join( - [ - shlex.quote(str(runner_script)), - "--core-module", - shlex.quote(CAD_CORE_MODULE[cad_core]), - "--input-path", - shlex.quote(str(source_asset)), - "--output-path", - shlex.quote(str(output_usd_path)), - "--config-path", - shlex.quote(str(config_path)), - ] - ) - - -def _core_command( - kit_executable: Path | None, - runner_script: Path | None, - source_asset: Path, - output_usd_path: Path, - config_path: Path | None, - cad_core: str, -) -> list[str]: - return [ - str(kit_executable) if kit_executable else "/kit/kit", - "--allow-root", - "--enable", - CAD_CORE_EXTENSION[cad_core], - "--/app/fastShutdown=1", - "--exec", - _core_exec_payload( - runner_script or Path(""), - cad_core, - source_asset, - output_usd_path, - config_path or Path(""), - ), - "--info", - ] - - -def _service_command( - kit_executable: Path | None, - process_script: Path | None, - source_asset: Path, - output_usd_path: Path, - config_path: Path | None, - cad_core: str, -) -> list[str]: - return [ - str(kit_executable) if kit_executable else "/kit/kit", - "--allow-root", - "--enable", - CAD_CORE_EXTENSION[cad_core], - "--/app/fastShutdown=1", - "--exec", - _kit_exec_payload( - process_script or Path("/omni/services/convert/cad/services/process") / CAD_PROCESS_SCRIPT[cad_core], - source_asset, - output_usd_path, - config_path or Path(""), - ), - "--info", - ] - - -def _sidecar_inputs(*paths: Path | None) -> list[str]: - return [str(path) for path in paths if path is not None] - - -def probe_source(source_asset: Path) -> dict[str, Any]: - source_asset = source_asset.resolve() - return { - "source_asset_path": str(source_asset), - "source_format": "cad", - "converter_skill": SKILL, - "converter_tool": KIT_CAD_CONVERTER_TOOL, - "supported": True, - "sidecar_inputs": [], - "warnings": [ - "Linux arm64 host detected; probing the Kit App Template CAD Converter fallback because upstream usd-convert-cad is not available for this architecture.", - f"Fallback does not maintain a local source-format allowlist; Kit App Template determines support at conversion time. Upstream Kit App Template: {KIT_APP_TEMPLATE_URL}.", - ], - "errors": [], - "install_hint": KIT_INSTALL_HINT, - } - - -def check_dependencies( - *, - kit_app_template_root: Path | None = None, - kit_build_dir: Path | None = None, - kit_executable: Path | None = None, - cad_service_extension_dir: Path | None = None, - execution_mode: str = "core", -) -> dict[str, Any]: - kit_root = _resolve_kit_root(kit_app_template_root) - build_dir = _resolve_build_dir(kit_build_dir, kit_root) - resolved_kit_executable = _resolve_kit_executable(kit_executable, build_dir) - service_extension_dir = _resolve_service_extension_dir(cad_service_extension_dir, build_dir) - execution_mode = execution_mode.lower() - - checks = [ - _check("python_available", True, f"Python executable: {sys.executable}"), - _check("host_is_arm64", is_arm64_host(), f"Host architecture: {platform.machine()}"), - _check( - "kit_app_template_root_exists", - kit_root.exists() and ((kit_root / "repo.sh").exists() or (kit_root / "repo.bat").exists()), - f"Kit App Template root: {kit_root}", - ), - _check( - "kit_app_template_build_dir_exists", - build_dir is not None and build_dir.exists(), - f"Kit App Template build directory: {build_dir or ''}", - ), - _check( - "kit_executable_exists", - resolved_kit_executable is not None and resolved_kit_executable.exists(), - f"Kit executable: {resolved_kit_executable or ''}", - ), - ] - if execution_mode not in {"core", "service"}: - checks.append(_check("kit_cad_execution_mode_supported", False, f"Unsupported execution mode: {execution_mode}")) - if execution_mode == "service": - process_script = _process_script_path(service_extension_dir, "hoops") - checks.extend( - [ - _check( - "cad_service_extension_dir_exists", - service_extension_dir is not None and service_extension_dir.exists(), - f"CAD service extension directory: {service_extension_dir or ''}", - ), - _check( - "cad_service_process_script_exists", - process_script is not None and process_script.exists(), - f"CAD service HOOPS process script: {process_script or ''}", - ), - ] - ) - - errors = [check["message"] for check in checks if not check["passed"]] - payload = { - "skill": SKILL, - "passed": not errors, - "runtime": "kit-app-template-cad-arm64-fallback", - "checks": checks, - "errors": errors, - "upstream_repo": KIT_APP_TEMPLATE_URL, - "kit_app_template_root": str(kit_root), - "kit_build_dir": str(build_dir) if build_dir is not None else "", - "kit_executable": str(resolved_kit_executable) if resolved_kit_executable is not None else "", - } - if errors: - payload["install_hint"] = KIT_INSTALL_HINT - return payload - - -def convert_with_kit_app_template( - source_asset: Path, - output_directory: Path, - *, - kit_app_template_root: Path | None = None, - kit_build_dir: Path | None = None, - kit_executable: Path | None = None, - cad_service_extension_dir: Path | None = None, - config_path: Path | None = None, - output_extension: str = ".usd", - execution_mode: str = "core", - fine: bool = False, - coarse: bool = False, - tessellation_chord: float = 0.01, - tessellation_angle: float = 30.0, - no_materials: bool = False, - single_mesh: bool = False, - no_meter_units: bool = False, - keep_hidden: bool = False, - timeout: int = 1800, -) -> dict[str, Any]: - source_asset = source_asset.resolve() - output_directory = output_directory.resolve() - output_extension = output_extension if output_extension.startswith(".") else f".{output_extension}" - output_usd_path = output_directory / f"{source_asset.stem}{output_extension}" - source_format = "cad" - kit_root = _resolve_kit_root(kit_app_template_root) - build_dir = _resolve_build_dir(kit_build_dir, kit_root) - resolved_kit_executable = _resolve_kit_executable(kit_executable, build_dir) - service_extension_dir = _resolve_service_extension_dir(cad_service_extension_dir, build_dir) - cad_core = _cad_core_for_source(source_asset) - execution_mode = execution_mode.lower() - process_script = _process_script_path(service_extension_dir, cad_core) if execution_mode == "service" else None - - warnings = [ - "Linux arm64 host detected; using Kit App Template CAD Converter fallback because upstream usd-convert-cad is not available for this architecture.", - f"Fallback runtime: {KIT_CAD_CONVERTER_TOOL}; upstream Kit App Template: {KIT_APP_TEMPLATE_URL}.", - f"CAD Converter extension docs: {CAD_CONVERTER_DOCS_URL}.", - "Default execution uses direct CAD core extension APIs, not the CAD service extension.", - f"CAD Converter service CLI docs for optional service mode: {CAD_CONVERTER_SERVICE_DOCS_URL}.", - ] - errors: list[str] = [] - if not source_asset.exists(): - errors.append(f"source asset does not exist: {source_asset}") - if output_extension.lower() not in USD_OUTPUT_SUFFIXES: - errors.append(f"unsupported USD output extension: {output_extension}") - if fine and coarse: - errors.append("--fine and --coarse are mutually exclusive") - if execution_mode not in {"core", "service"}: - errors.append(f"unsupported CAD execution mode: {execution_mode}. Use core or service.") - if not kit_root.exists() or not ((kit_root / "repo.sh").exists() or (kit_root / "repo.bat").exists()): - errors.append(f"Kit App Template checkout was not found or is incomplete: {kit_root}") - if build_dir is not None and not build_dir.exists() and resolved_kit_executable is None: - dependency_hint = ( - "omni.services.convert.cad and the CAD core extensions" - if execution_mode == "service" - else f"{CAD_CORE_EXTENSION[cad_core]} or the bundled omni.kit.converter.cad extension" - ) - errors.append( - f"Kit App Template build directory was not found: {build_dir}. Run the upstream build flow from " - f"{KIT_APP_TEMPLATE_URL} and include {dependency_hint} in the app dependencies." - ) - if resolved_kit_executable is None or not resolved_kit_executable.exists(): - errors.append( - "Kit executable was not found. Set --kit-executable, KIT_APP_TEMPLATE_KIT_EXECUTABLE, " - "KIT_EXECUTABLE, or build Kit App Template so _build//release/kit/kit exists." - ) - if execution_mode == "service" and (service_extension_dir is None or not service_extension_dir.exists()): - errors.append( - "omni.services.convert.cad extension cache was not found. Add omni.services.convert.cad to the " - "Kit App Template app dependencies and build/precache the app, or set --cad-service-extension-dir." - ) - if execution_mode == "service" and (process_script is None or not process_script.exists()): - errors.append( - f"CAD service process script for {cad_core} conversion was not found: " - f"{process_script or ''}" - ) - - effective_config_path = config_path.resolve() if config_path else None - if effective_config_path is not None and not effective_config_path.exists(): - errors.append(f"CAD converter config path does not exist: {effective_config_path}") - - runner_script = None - command = ( - _service_command( - resolved_kit_executable, - process_script, - source_asset, - output_usd_path, - effective_config_path, - cad_core, - ) - if execution_mode == "service" - else _core_command( - resolved_kit_executable, - runner_script, - source_asset, - output_usd_path, - effective_config_path, - cad_core, - ) - ) - - if errors: - return { - "source_asset_path": str(source_asset), - "source_format": source_format, - "converter_skill": SKILL, - "converter_tool": KIT_CAD_CONVERTER_TOOL, - "converter_command": command, - "output_directory": str(output_directory), - "output_usd_path": "", - "generated_files": [], - "sidecar_inputs": _sidecar_inputs(kit_root, build_dir, resolved_kit_executable, service_extension_dir, effective_config_path), - "warnings": warnings, - "errors": errors, - "install_hint": KIT_INSTALL_HINT, - "next_step": NEXT_STEP, - } - - output_directory.mkdir(parents=True, exist_ok=True) - if effective_config_path is None: - effective_config_path = _write_converter_config( - output_directory, - source_asset, - fine=fine, - coarse=coarse, - tessellation_chord=tessellation_chord, - tessellation_angle=tessellation_angle, - no_materials=no_materials, - single_mesh=single_mesh, - no_meter_units=no_meter_units, - keep_hidden=keep_hidden, - ) - if execution_mode == "core": - runner_script = _write_core_runner_script(output_directory, source_asset) - command = _core_command( - resolved_kit_executable, - runner_script, - source_asset, - output_usd_path, - effective_config_path, - cad_core, - ) - else: - command = _service_command( - resolved_kit_executable, - process_script, - source_asset, - output_usd_path, - effective_config_path, - cad_core, - ) - - env = os.environ.copy() - env.setdefault("ACCEPT_EULA", "Y") - env.setdefault("OMNI_KIT_ACCEPT_EULA", "yes") - completed = subprocess.run( - command, - cwd=str(build_dir or resolved_kit_executable.parent), - env=env, - capture_output=True, - text=True, - timeout=timeout, - check=False, - ) - - if completed.returncode != 0: - detail = completed.stderr.strip() or completed.stdout.strip() - if not detail: - detail = f"Kit App Template CAD conversion exited with {completed.returncode}" - errors.append(_compact(detail)) - if completed.stdout.strip() and completed.returncode == 0: - warnings.append(_compact(completed.stdout)) - if completed.stderr.strip() and completed.returncode == 0: - warnings.append(_compact(completed.stderr)) - if not output_usd_path.exists(): - errors.append(f"converter did not produce expected USD output: {output_usd_path}") - - return { - "source_asset_path": str(source_asset), - "source_format": source_format, - "converter_skill": SKILL, - "converter_tool": KIT_CAD_CONVERTER_TOOL, - "converter_command": command, - "output_directory": str(output_directory), - "output_usd_path": str(output_usd_path) if output_usd_path.exists() else "", - "generated_files": discover_generated_files(output_directory), - "sidecar_inputs": _sidecar_inputs( - kit_root, - build_dir, - resolved_kit_executable, - service_extension_dir, - effective_config_path, - runner_script, - ), - "warnings": warnings, - "errors": errors, - "install_hint": KIT_INSTALL_HINT if errors else "", - "next_step": NEXT_STEP, - } diff --git a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-cad/scripts/run.py b/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-cad/scripts/run.py index d9d5378f..f2767e41 100644 --- a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-cad/scripts/run.py +++ b/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-cad/scripts/run.py @@ -5,53 +5,120 @@ from __future__ import annotations import argparse -import ast from dataclasses import dataclass, field import json import os from pathlib import Path +import platform +import re import subprocess import sys from typing import Any sys.path.insert(0, str(Path(__file__).resolve().parents[5] / "shared")) -import kit_app_template_cad -from preflight_manifest import load_preflight_manifest, preflight_required, preflight_status_check, ready_path_from_runtime +from preflight_manifest import ( + load_preflight_manifest, + preflight_required, + preflight_status_check, + ready_executable_from_runtime, + ready_path_from_upstream, +) from script_utils import emit_json_report, subprocess_output, tail_text -from usd_convert_cad_diagnostics import summarize_usd_convert_cad_validation_failure +from upstream_versions import UPSTREAM_VERSION_MANIFEST_PATH, package_requirement, package_version SKILL = "usd-convert-cad" TOOL = "usd-convert-cad" NEXT_STEP = "validate-usd-minimum" UPSTREAM_REPO_URL = "https://github.com/NVIDIA-Omniverse/usd-convert-cad" -UPSTREAM_SKILL_URL = "https://github.com/NVIDIA-Omniverse/usd-convert-cad/blob/main/.agents/skills/usd-convert-cad/SKILL.md" -UPSTREAM_ROOT_ENV = "PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT" +UPSTREAM_SKILL_URL = "https://github.com/NVIDIA-Omniverse/usd-convert-cad/blob/v0.2.0/skills/omniverse-cad-to-usd/SKILL.md" + +# usd-convert-cad 0.2 ships as a self-contained Python wheel. It exposes the +# console command `usd-convert-cad` (equivalently `python -m usd_convert_cad`) +# with `-i -o ` and bundles its own OpenUSD (`pxr`) runtime, so +# it must be installed into an isolated Python 3.12 interpreter. There is no Kit +# app, no extension-registry pull, and no upstream `convert.py`/`validate.py`. +WHEEL_MODULE = "usd_convert_cad" +PIP_PACKAGE = package_requirement("usd-convert-cad") +EXPECTED_WHEEL_VERSION = package_version("usd-convert-cad") +CONVERTER_PYTHON_ENV = "USD_CONVERT_CAD_PYTHON" INSTALL_HINT = ( - 'export PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT="${PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT:-$HOME/.physical-ai-skill-hub/upstreams}" ' - "&& mkdir -p \"$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT\" " - f"&& git clone {UPSTREAM_REPO_URL} \"$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/usd-convert-cad\" " - "&& cd \"$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/usd-convert-cad\" " - "&& OMNI_KIT_ACCEPT_EULA=yes python install.py && python validate.py" + "Install the usd-convert-cad wheel into an isolated Python 3.12 environment, then point this " + f"reference at that interpreter with --usd-convert-cad-python or {CONVERTER_PYTHON_ENV}: " + 'python3.12 -m venv "$HOME/.omniverse-cad-to-simready/venvs/usd-convert-cad" ' + '&& "$HOME/.omniverse-cad-to-simready/venvs/usd-convert-cad/bin/python" -m pip install ' + f"{PIP_PACKAGE} --extra-index-url https://pypi.nvidia.com " + f'&& export {CONVERTER_PYTHON_ENV}="$HOME/.omniverse-cad-to-simready/venvs/usd-convert-cad/bin/python"' ) -USD_OUTPUT_SUFFIXES = {".usd", ".usda", ".usdc", ".usdz"} -UPSTREAM_PREFLIGHT_TIMEOUT_SECONDS = 600 -BACKEND_ALIASES = { - "auto": "auto", - "cad": "auto", - "usd-convert-cad": "auto", - "jt": "jt_core", - "jt_core": "jt_core", - "omni.kit.converter.jt_core": "jt_core", - "dgn": "dgn_core", - "dgn_core": "dgn_core", - "omni.kit.converter.dgn_core": "dgn_core", - "hoops": "hoops_core", - "hoops_core": "hoops_core", - "omni.kit.converter.hoops_core": "hoops_core", -} -ARM64_BACKEND_ALIASES = {*BACKEND_ALIASES, "kat", "kit", "kit-app-template"} + +USD_OUTPUT_SUFFIXES = {".usd", ".usda", ".usdc"} +IMPORT_CHECK_TIMEOUT_SECONDS = 120 + +# The authoritative source-format list lives in the usd-convert-cad SKILL.md +# Supported Formats table. When a checkout of that repo is available, the probe +# parses the table live so upstream format changes flow through automatically; +# see resolve_skill_md / parse_supported_suffixes_from_skill below. These env +# vars/paths locate that checkout (they never affect the conversion runtime, +# which is always the installed wheel). +CONVERTER_ROOT_ENV = "USD_CONVERT_CAD_ROOT" +UPSTREAM_ROOT_ENV = "OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT" +SKILL_MD_ENV = "USD_CONVERT_CAD_SKILL" +# Candidate SKILL.md locations within a usd-convert-cad checkout, most specific +# first. A glob fallback keeps this working if the upstream layout moves. +SKILL_MD_CANDIDATES = ( + Path("skills") / "omniverse-cad-to-usd" / "SKILL.md", + Path(".agents") / "skills" / "usd-convert-cad" / "SKILL.md", +) + +# Fallback snapshot of the usd-convert-cad Supported Formats table, used only +# when no SKILL.md checkout is reachable (for example, fully offline runs). +# Keep it in sync with the upstream SKILL.md. +SUPPORTED_CAD_SUFFIXES = frozenset( + { + ".jt", + ".dgn", + ".catpart", + ".catproduct", + ".cgr", + ".3dxml", + ".ifc", + ".ifczip", + ".prt", + ".asm", + ".xmt", + ".x_t", + ".x_b", + ".xmt_txt", + ".sldprt", + ".sldasm", + ".stl", + ".ipt", + ".iam", + ".dwg", + ".dxf", + ".rvt", + ".rfa", + ".par", + ".pwd", + ".psm", + ".stp", + ".step", + ".igs", + ".iges", + ".3dm", + ".dae", + ".fbx", + ".obj", + ".3ds", + ".3mf", + ".gltf", + ".glb", + ".sat", + ".sab", + } +) +LINUX_AARCH64_UNSUPPORTED_SUFFIXES = frozenset({".dwg", ".dxf", ".rvt", ".rfa"}) @dataclass(frozen=True) @@ -126,54 +193,63 @@ def to_markdown(self) -> str: lines.extend(f"- {error}" for error in self.errors) if not self.errors: lines.append("- None") - if self.diagnostics: - lines.extend(["", "## Diagnostics", ""]) - for diagnostic in self.diagnostics: - summary = diagnostic.get("summary") or diagnostic.get("kind") or "diagnostic" - lines.append(f"- {summary}") - recovery_hint = diagnostic.get("recovery_hint") - if recovery_hint: - lines.append(f" Recovery: {recovery_hint}") if self.install_hint: lines.extend(["", "## Install Hint", "", self.install_hint]) lines.append("") return "\n".join(lines) -def default_upstream_root() -> Path: - root = os.environ.get(UPSTREAM_ROOT_ENV) - if root: - return Path(root).expanduser() / "usd-convert-cad" - return Path.home() / ".physical-ai-skill-hub" / "upstreams" / "usd-convert-cad" +def _absolute_without_resolving_symlinks(path: Path) -> Path: + """Return an absolute path while preserving a virtualenv Python symlink.""" + return Path(os.path.abspath(os.fspath(path.expanduser()))) + +def resolve_converter_python(explicit: Path | None) -> Path: + """Resolve the Python interpreter that has the usd-convert-cad wheel. -def resolve_usd_convert_cad_root(explicit: Path | None) -> Path: + Because the wheel bundles its own ``pxr`` runtime it is expected to live in + a dedicated interpreter rather than the orchestrator's Python. Resolution + order: explicit ``--usd-convert-cad-python`` -> ``USD_CONVERT_CAD_PYTHON`` + env -> preflight manifest ``usd_convert_cad`` runtime executable -> the + current interpreter as a last resort. + """ if explicit is not None: - return explicit.expanduser().resolve() + return _absolute_without_resolving_symlinks(explicit) + env_python = os.environ.get(CONVERTER_PYTHON_ENV) + if env_python: + return _absolute_without_resolving_symlinks(Path(env_python)) manifest, _, _ = load_preflight_manifest() - manifest_root = ready_path_from_runtime(manifest, "usd_convert_cad") - if manifest_root is not None: - return manifest_root - env_root = os.environ.get("USD_CONVERT_CAD_ROOT") - if env_root: - return Path(env_root).expanduser().resolve() - default = default_upstream_root().expanduser() - if default.exists(): - return default.resolve() - legacy = Path("~/.usd-convert-cad").expanduser() - if legacy.exists(): - return legacy.resolve() - return default.resolve() + manifest_python = ready_executable_from_runtime(manifest, "usd_convert_cad") + if manifest_python: + return _absolute_without_resolving_symlinks(Path(manifest_python)) + return _absolute_without_resolving_symlinks(Path(sys.executable)) -def discover_generated_files(output_directory: Path) -> list[str]: - if not output_directory.exists(): - return [] - return sorted( - str(path.relative_to(output_directory)) - for path in output_directory.rglob("*") - if path.is_file() - ) +def wheel_version(python: Path) -> tuple[str | None, str]: + """Return the installed wheel version, or ``None`` with a failure detail.""" + command = [ + str(python), + "-c", + ( + "import usd_convert_cad as m; " + "print(getattr(m, '__version__', None) or " + "(m.get_version() if hasattr(m, 'get_version') else 'unknown'))" + ), + ] + try: + completed = subprocess.run( + command, + capture_output=True, + text=True, + timeout=IMPORT_CHECK_TIMEOUT_SECONDS, + check=False, + ) + except (OSError, subprocess.TimeoutExpired) as exc: + return None, f"could not run {python}: {exc}" + if completed.returncode == 0: + return completed.stdout.strip() or "unknown", "" + detail = subprocess_output(completed.stdout, completed.stderr) or f"exit {completed.returncode}" + return None, tail_text(detail) def real_suffix(source_asset: Path) -> str: @@ -183,56 +259,110 @@ def real_suffix(source_asset: Path) -> str: return suffix -def parse_upstream_cad_suffixes(upstream_root: Path) -> set[str] | None: - formats_path = upstream_root / "src" / "usd_convert_cad" / "formats.py" - try: - module = ast.parse(formats_path.read_text(encoding="utf-8")) - except (OSError, SyntaxError): - return None - - for node in module.body: - if isinstance(node, ast.Assign): - names = {target.id for target in node.targets if isinstance(target, ast.Name)} - value = node.value - elif isinstance(node, ast.AnnAssign): - names = {node.target.id} if isinstance(node.target, ast.Name) else set() - value = node.value - else: - continue - if value is None or not names.intersection({"ROUTES", "SUPPORTED_FORMATS"}): - continue - return parse_format_info_suffixes(value) +def default_upstream_checkout() -> Path: + root = os.environ.get(UPSTREAM_ROOT_ENV) + if root: + return Path(root).expanduser() / "usd-convert-cad" + return Path.home() / ".omniverse-cad-to-simready" / "upstreams" / "usd-convert-cad" + + +def _skill_md_in_checkout(root: Path) -> Path | None: + root = root.expanduser() + if root.is_file(): + return root + for relative in SKILL_MD_CANDIDATES: + candidate = root / relative + if candidate.is_file(): + return candidate + if root.exists(): + matches = sorted(root.glob("**/SKILL.md")) + if matches: + return matches[0] return None -def parse_format_info_suffixes(value: ast.AST) -> set[str] | None: - if not isinstance(value, (ast.Tuple, ast.List)): - return None +def resolve_skill_md(explicit_skill: Path | None, explicit_root: Path | None) -> Path | None: + """Locate the usd-convert-cad SKILL.md that defines the Supported Formats. + + Order: explicit ``--usd-convert-cad-skill`` -> ``USD_CONVERT_CAD_SKILL`` env + -> explicit ``--usd-convert-cad-root`` -> preflight manifest upstream path + -> ``USD_CONVERT_CAD_ROOT`` env -> the default shared upstream checkout. + Returns ``None`` when no checkout is reachable (the probe then falls back to + the built-in snapshot). + """ + for candidate_root in (explicit_skill, os.environ.get(SKILL_MD_ENV), explicit_root): + if candidate_root: + found = _skill_md_in_checkout(Path(candidate_root)) + if found is not None: + return found + manifest, _, _ = load_preflight_manifest() + upstream_path = ready_path_from_upstream(manifest, "usd_convert_cad") + if upstream_path is not None: + found = _skill_md_in_checkout(upstream_path) + if found is not None: + return found + env_root = os.environ.get(CONVERTER_ROOT_ENV) + if env_root: + found = _skill_md_in_checkout(Path(env_root)) + if found is not None: + return found + return _skill_md_in_checkout(default_upstream_checkout()) + +def parse_supported_suffixes_from_skill(skill_md: Path) -> set[str] | None: + """Parse the Supported Formats markdown table from a usd-convert-cad SKILL.md.""" + try: + text = skill_md.read_text(encoding="utf-8") + except OSError: + return None + collecting = False suffixes: set[str] = set() - for route_node in value.elts: - if not isinstance(route_node, ast.Call) or not route_node.args: - return None - try: - file_types = ast.literal_eval(route_node.args[0]) - except (ValueError, SyntaxError): - return None - if isinstance(file_types, str): - suffixes.add(file_types.lower()) - else: - suffixes.update(str(file_type).lower() for file_type in file_types) + for line in text.splitlines(): + stripped = line.strip() + if stripped.lower().startswith("## "): + collecting = stripped.lower() == "## supported formats" + continue + if not collecting or not stripped.startswith("|"): + continue + first_column = stripped.strip("|").split("|")[0] + for token in re.findall(r"`([^`]+)`", first_column): + token = token.strip().lower() + if token.startswith("."): + suffixes.add(token) return suffixes or None -def supported_cad_suffixes(upstream_root: Path) -> set[str] | None: - return parse_upstream_cad_suffixes(upstream_root) +def supported_cad_suffixes(skill_md: Path | None) -> tuple[frozenset[str], str]: + """Return (suffixes, human-readable source) from SKILL.md or the snapshot.""" + if skill_md is not None: + parsed = parse_supported_suffixes_from_skill(skill_md) + if parsed: + return frozenset(parsed), f"usd-convert-cad SKILL.md ({skill_md})" + return SUPPORTED_CAD_SUFFIXES, "built-in snapshot of the usd-convert-cad Supported Formats table" + + +def platform_limitation(suffix: str) -> str | None: + machine = platform.machine().lower() + if ( + platform.system().lower() == "linux" + and machine in {"aarch64", "arm64"} + and suffix in LINUX_AARCH64_UNSUPPORTED_SUFFIXES + ): + return ( + f"usd-convert-cad does not support {suffix} on linux-aarch64; " + "the upstream DWG/Revit readers are unavailable on this platform" + ) + return None -def probe_source(source_asset: Path, *, usd_convert_cad_root: Path | None = None) -> dict[str, Any]: +def probe_source( + source_asset: Path, + *, + converter_python: Path | None = None, + skill_md: Path | None = None, +) -> dict[str, Any]: source_asset = source_asset.resolve() - if kit_app_template_cad.is_arm64_host(): - return kit_app_template_cad.probe_source(source_asset) - if preflight_required() and usd_convert_cad_root is None: + if preflight_required() and converter_python is None: preflight_check = preflight_status_check("usd-convert-cad", "usd_convert_cad") if not preflight_check["passed"]: return { @@ -246,263 +376,138 @@ def probe_source(source_asset: Path, *, usd_convert_cad_root: Path | None = None "errors": [preflight_check["message"]], "install_hint": preflight_check["message"], } - upstream_root = resolve_usd_convert_cad_root(usd_convert_cad_root) suffix = real_suffix(source_asset) - suffixes = supported_cad_suffixes(upstream_root) - errors: list[str] = [] - warnings = [ - f"Capability lookup is read from upstream usd-convert-cad formats.py: {UPSTREAM_REPO_URL}.", - ] - supported = False - if suffixes is None: - errors.append( - "unable to read upstream usd-convert-cad supported formats from " - f"{upstream_root / 'src' / 'usd_convert_cad' / 'formats.py'}" - ) - else: - supported = suffix in suffixes - if not supported: - warnings.append(f"upstream usd-convert-cad does not list source suffix: {suffix or 'unknown'}") - + suffixes, source_label = supported_cad_suffixes(skill_md) + limitation = platform_limitation(suffix) + supported = suffix in suffixes and limitation is None + warnings = [f"Capability lookup uses the {source_label}."] + if limitation: + warnings.append(limitation) + elif not supported: + warnings.append(f"usd-convert-cad does not list source suffix: {suffix or 'unknown'}") return { "source_asset_path": str(source_asset), "source_format": "cad" if supported else "unknown", "converter_skill": SKILL, "converter_tool": TOOL, "supported": supported, - "sidecar_inputs": [str(upstream_root)], + "sidecar_inputs": [], "warnings": warnings, - "errors": errors, - "install_hint": INSTALL_HINT if suffixes is None else "", + "errors": [], + "install_hint": "", } -def normalize_backend(backend: str) -> tuple[str, str | None]: - value = backend.strip().lower() - if value in BACKEND_ALIASES: - return BACKEND_ALIASES[value], None - return value, ( - f"unsupported backend: {backend}. CAD conversion is restricted to upstream " - "NVIDIA usd-convert-cad with Kit converter core extensions." - ) - - def cad_to_usd( source_asset: Path, output_directory: Path, *, - backend: str = "auto", - usd_convert_cad_root: Path | None = None, - kit_app_template_root: Path | None = None, - kit_build_dir: Path | None = None, - kit_executable: Path | None = None, - cad_service_extension_dir: Path | None = None, - config_path: Path | None = None, - execution_mode: str = "core", + converter_python: Path | None = None, + skill_md: Path | None = None, output_extension: str = ".usd", - fine: bool = False, - coarse: bool = False, - tessellation_chord: float = 0.01, - tessellation_angle: float = 30.0, - no_materials: bool = False, - single_mesh: bool = False, - no_meter_units: bool = False, - keep_hidden: bool = False, timeout: int = 1800, + extra_args: list[str] | None = None, + legacy_backend: str = "auto", ) -> ConversionReport: source_asset = source_asset.resolve() output_directory = output_directory.resolve() output_extension = output_extension if output_extension.startswith(".") else f".{output_extension}" output_usd_path = output_directory / f"{source_asset.stem}{output_extension}" - if kit_app_template_cad.is_arm64_host(): - backend_value = backend.strip().lower() - if backend_value not in ARM64_BACKEND_ALIASES: - return ConversionReport( - source_asset_path=str(source_asset), - source_format="cad", - converter_skill=SKILL, - converter_tool="none", - converter_command=[ - sys.executable, - str(Path(__file__).resolve()), - str(source_asset), - str(output_directory), - "--backend", - backend, - ], - output_directory=str(output_directory), - output_usd_path="", - generated_files=[], - errors=[ - f"unsupported backend: {backend}. Linux arm64 CAD conversion is restricted to the Kit App Template CAD Converter fallback." - ], - install_hint=kit_app_template_cad.KIT_INSTALL_HINT, - ) - payload = kit_app_template_cad.convert_with_kit_app_template( - source_asset, - output_directory, - kit_app_template_root=kit_app_template_root, - kit_build_dir=kit_build_dir, - kit_executable=kit_executable, - cad_service_extension_dir=cad_service_extension_dir, - config_path=config_path, - output_extension=output_extension, - execution_mode=execution_mode, - fine=fine, - coarse=coarse, - tessellation_chord=tessellation_chord, - tessellation_angle=tessellation_angle, - no_materials=no_materials, - single_mesh=single_mesh, - no_meter_units=no_meter_units, - keep_hidden=keep_hidden, - timeout=timeout, - ) - return ConversionReport(**payload) + extra_args = list(extra_args or []) - upstream_root = resolve_usd_convert_cad_root(usd_convert_cad_root) - normalized_backend, backend_error = normalize_backend(backend) - upstream_report = output_directory / f"{source_asset.stem}_usd_convert_cad_status.json" - upstream_log = upstream_report.with_suffix(".log") - upstream_validate_log = output_directory / f"{source_asset.stem}_usd_convert_cad_validate.log" + python = resolve_converter_python(converter_python) command = [ - sys.executable, - str(upstream_root / "convert.py"), + str(python), + "-m", + WHEEL_MODULE, + "-i", str(source_asset), + "-o", str(output_usd_path), - "--report", - str(upstream_report), - "--quiet", - "--log", - str(upstream_log), + *extra_args, ] warnings = [ - f"Delegating CAD conversion to upstream {TOOL}: {UPSTREAM_REPO_URL}.", + f"Delegating CAD conversion to the {TOOL} wheel: {UPSTREAM_REPO_URL}.", f"Upstream agent skill reference: {UPSTREAM_SKILL_URL}.", + f"Converter interpreter: {python}.", ] - if normalized_backend != "auto": + if legacy_backend != "auto": warnings.append( - f"Upstream usd-convert-cad no longer exposes backend selection; using its default converter for requested backend `{backend}`." + "usd-convert-cad 0.2 no longer exposes backend selection; " + f"ignored legacy --backend {legacy_backend}." ) errors: list[str] = [] - if preflight_required() and usd_convert_cad_root is None: + + if preflight_required() and converter_python is None: preflight_check = preflight_status_check("usd-convert-cad", "usd_convert_cad") if not preflight_check["passed"]: errors.append(preflight_check["message"]) - if backend_error: - errors.append(backend_error) - suffixes = supported_cad_suffixes(upstream_root) - if suffixes is None: - errors.append( - "unable to read upstream usd-convert-cad supported formats; " - "set USD_CONVERT_CAD_ROOT or PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT" - ) - elif real_suffix(source_asset) not in suffixes: - errors.append(f"unsupported CAD source format: {real_suffix(source_asset) or 'unknown'}") + + suffixes, source_label = supported_cad_suffixes(skill_md) + warnings.append(f"Supported-format source: {source_label}.") + source_suffix = real_suffix(source_asset) + if source_suffix not in suffixes: + errors.append(f"unsupported CAD source format: {source_suffix or 'unknown'}") + limitation = platform_limitation(source_suffix) + if limitation: + errors.append(limitation) if not source_asset.exists(): errors.append(f"source asset does not exist: {source_asset}") if output_extension.lower() not in USD_OUTPUT_SUFFIXES: errors.append(f"unsupported USD output extension: {output_extension}") - if not upstream_root.exists(): + + version, version_error = wheel_version(python) + if version is None: errors.append( - f"usd-convert-cad checkout was not found: {upstream_root}. Clone {UPSTREAM_REPO_URL} " - "there or set USD_CONVERT_CAD_ROOT. You can also set " - f"{UPSTREAM_ROOT_ENV} to change the shared upstream checkout root." + f"usd-convert-cad wheel is not importable with {python}: {version_error}" ) - elif not (upstream_root / "convert.py").exists(): - errors.append(f"usd-convert-cad convert.py was not found under checkout: {upstream_root}") - elif not (upstream_root / "validate.py").exists(): - errors.append(f"usd-convert-cad validate.py was not found under checkout: {upstream_root}") - if errors: - install_hint = INSTALL_HINT if not upstream_root.exists() or not (upstream_root / "convert.py").exists() else "" - return _report(source_asset, output_directory, output_usd_path, command, upstream_root, warnings, errors, install_hint) - - output_directory.mkdir(parents=True, exist_ok=True) - sidecar_inputs = [str(upstream_root), str(upstream_validate_log)] - validation_error, validation_diagnostic = validate_upstream_usd_convert_cad(upstream_root, upstream_validate_log) - if validation_error: - errors.append(validation_error) - diagnostics = [validation_diagnostic] if validation_diagnostic else [] - return _report( - source_asset, - output_directory, - output_usd_path, - command, - upstream_root, - warnings, - errors, - sidecar_inputs=sidecar_inputs, - diagnostics=diagnostics, + elif version != EXPECTED_WHEEL_VERSION: + errors.append( + f"usd-convert-cad requires version {EXPECTED_WHEEL_VERSION}, found {version}; " + f"see {UPSTREAM_VERSION_MANIFEST_PATH}" ) + else: + warnings.append(f"usd-convert-cad wheel version: {version}.") - env = os.environ.copy() - env.setdefault("OMNI_KIT_ACCEPT_EULA", "yes") - completed = subprocess.run( - command, - cwd=str(upstream_root), - env=env, - capture_output=True, - text=True, - timeout=timeout, - check=False, - ) - if completed.returncode != 0: - detail = completed.stderr.strip() or completed.stdout.strip() or f"{TOOL} exited with {completed.returncode}" - errors.append(detail) - if not output_usd_path.exists() and not errors: - errors.append(f"converter did not produce expected USD output: {output_usd_path}") - for sidecar in (upstream_report, upstream_log): - if sidecar.exists(): - sidecar_inputs.append(str(sidecar)) - return _report(source_asset, output_directory, output_usd_path, command, upstream_root, warnings, errors, sidecar_inputs=sidecar_inputs) - + if errors: + install_hint = INSTALL_HINT if version != EXPECTED_WHEEL_VERSION else "" + return _report(source_asset, output_directory, output_usd_path, command, python, warnings, errors, install_hint) -def validate_upstream_usd_convert_cad(upstream_root: Path, log_path: Path) -> tuple[str | None, dict[str, Any] | None]: - command = [sys.executable, str(upstream_root / "validate.py")] + output_directory.mkdir(parents=True, exist_ok=True) env = os.environ.copy() - env.setdefault("OMNI_KIT_ACCEPT_EULA", "yes") try: completed = subprocess.run( command, - cwd=str(upstream_root), - env=env, capture_output=True, text=True, - timeout=UPSTREAM_PREFLIGHT_TIMEOUT_SECONDS, + timeout=timeout, + env=env, check=False, ) except subprocess.TimeoutExpired as exc: - output = subprocess_output(getattr(exc, "stdout", ""), getattr(exc, "stderr", "")) - _write_text(log_path, output) - return ( - "upstream usd-convert-cad readiness validation timed out after " - f"{UPSTREAM_PREFLIGHT_TIMEOUT_SECONDS}s. Resolve the upstream usd-convert-cad runtime and rerun validate.py.", - None, + detail = subprocess_output(getattr(exc, "stdout", ""), getattr(exc, "stderr", "")) + errors.append( + f"usd-convert-cad timed out after {timeout}s." + + (f" Output: {tail_text(detail)}" if detail else "") ) + return _report(source_asset, output_directory, output_usd_path, command, python, warnings, errors) - output = subprocess_output(completed.stdout, completed.stderr) - _write_text(log_path, output) - if completed.returncode == 0: - return None, None - detail = tail_text(output) or f"validate.py exited with {completed.returncode}" - diagnostic = summarize_usd_convert_cad_validation_failure(output, completed.returncode) - if diagnostic: - return ( - "upstream usd-convert-cad readiness validation failed " - f"(exit {completed.returncode}): {diagnostic['summary']} " - f"{diagnostic['recovery_hint']} Output: {detail}", - diagnostic, - ) - return ( - "upstream usd-convert-cad readiness validation failed " - f"(exit {completed.returncode}): {detail}. Resolve the upstream usd-convert-cad runtime and rerun validate.py.", - None, - ) + if completed.returncode != 0: + detail = subprocess_output(completed.stdout, completed.stderr) or f"{TOOL} exited with {completed.returncode}" + errors.append(tail_text(detail)) + if not output_usd_path.exists() and not errors: + errors.append(f"converter did not produce expected USD output: {output_usd_path}") + return _report(source_asset, output_directory, output_usd_path, command, python, warnings, errors) -def _write_text(path: Path, text: str) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text((text or "") + "\n", encoding="utf-8") +def discover_generated_files(output_directory: Path) -> list[str]: + if not output_directory.exists(): + return [] + return sorted( + str(path.relative_to(output_directory)) + for path in output_directory.rglob("*") + if path.is_file() + ) def _report( @@ -510,13 +515,10 @@ def _report( output_directory: Path, output_usd_path: Path, command: list[str], - upstream_root: Path, + converter_python: Path, warnings: list[str], errors: list[str], install_hint: str = "", - *, - sidecar_inputs: list[str] | None = None, - diagnostics: list[dict[str, Any]] | None = None, ) -> ConversionReport: return ConversionReport( source_asset_path=str(source_asset), @@ -527,10 +529,9 @@ def _report( output_directory=str(output_directory), output_usd_path=str(output_usd_path) if output_usd_path.exists() else "", generated_files=discover_generated_files(output_directory), - sidecar_inputs=sidecar_inputs or [str(upstream_root)], + sidecar_inputs=[str(converter_python)], warnings=warnings, errors=errors, - diagnostics=diagnostics or [], install_hint=install_hint, ) @@ -556,40 +557,49 @@ def emit_probe(payload: dict[str, Any], *, report_path: Path | None = None) -> N def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(description="Convert supported source assets to OpenUSD through upstream usd-convert-cad.") + parser = argparse.ArgumentParser( + description="Convert supported source assets to OpenUSD with the usd-convert-cad wheel." + ) parser.add_argument("source_asset", type=Path) parser.add_argument("output_directory", type=Path, nargs="?") - parser.add_argument("--probe", action="store_true", help="Report whether upstream usd-convert-cad claims this source format.") + parser.add_argument("--probe", action="store_true", help="Report whether usd-convert-cad claims this source format.") + parser.add_argument( + "--usd-convert-cad-python", + type=Path, + help=( + "Python interpreter that has the usd-convert-cad wheel installed. " + f"Defaults to {CONVERTER_PYTHON_ENV}, the preflight manifest, then this interpreter." + ), + ) parser.add_argument("--backend", default="auto", help=argparse.SUPPRESS) - parser.add_argument("--usd-convert-cad-root", type=Path) - parser.add_argument("--kit-app-template-root", type=Path, help="Linux arm64 fallback: local Kit App Template checkout path.") - parser.add_argument("--kit-build-dir", type=Path, help="Linux arm64 fallback: built Kit App Template _build//release directory.") - parser.add_argument("--kit-executable", type=Path, help="Linux arm64 fallback: built Kit executable.") - parser.add_argument("--cad-service-extension-dir", type=Path, help="Linux arm64 fallback service mode: omni.services.convert.cad extension directory.") - parser.add_argument("--config-path", type=Path, help="Linux arm64 fallback: optional CAD Converter config JSON path.") parser.add_argument( - "--execution-mode", - default="core", - choices=["core", "service"], - help="Linux arm64 fallback: use direct CAD core extension APIs or CAD service process scripts.", + "--usd-convert-cad-root", + type=Path, + help=( + "Path to a usd-convert-cad checkout used only to read its SKILL.md Supported Formats " + f"table. Defaults to {SKILL_MD_ENV}/{CONVERTER_ROOT_ENV}, the preflight manifest, then " + "the shared upstream checkout. Falls back to a built-in snapshot when unavailable." + ), + ) + parser.add_argument( + "--usd-convert-cad-skill", + type=Path, + help="Explicit path to the usd-convert-cad SKILL.md (overrides --usd-convert-cad-root discovery).", ) parser.add_argument("--output-extension", default=".usd", choices=sorted(USD_OUTPUT_SUFFIXES)) - quality = parser.add_mutually_exclusive_group() - quality.add_argument("--fine", action="store_true", help="Linux arm64 fallback: use fine CAD tessellation.") - quality.add_argument("--coarse", action="store_true", help="Linux arm64 fallback: use coarse CAD tessellation.") - parser.add_argument("--tessellation-chord", type=float, default=0.01, help="Linux arm64 fallback CAD tessellation chord.") - parser.add_argument("--tessellation-angle", type=float, default=30.0, help="Linux arm64 fallback CAD tessellation angle in degrees.") - parser.add_argument("--no-materials", action="store_true", help="Linux arm64 fallback: skip material import.") - parser.add_argument("--single-mesh", action="store_true", help="Linux arm64 fallback: request one mesh.") - parser.add_argument("--no-meter-units", action="store_true", help="Linux arm64 fallback: do not force meters per unit.") - parser.add_argument("--keep-hidden", action="store_true", help="Linux arm64 fallback: convert hidden CAD entities.") parser.add_argument("--timeout", type=int, default=1800) parser.add_argument("--report", type=Path, help="Write a JSON report to this path.") parser.add_argument("--markdown-report", type=Path, help="Write a Markdown report to this path.") - args = parser.parse_args(argv) + args, passthrough = parser.parse_known_args(argv) + + skill_md = resolve_skill_md(args.usd_convert_cad_skill, args.usd_convert_cad_root) if args.probe: - payload = probe_source(args.source_asset, usd_convert_cad_root=args.usd_convert_cad_root) + payload = probe_source( + args.source_asset, + converter_python=args.usd_convert_cad_python, + skill_md=skill_md, + ) emit_probe(payload, report_path=args.report) return 0 if payload["supported"] else 1 if args.output_directory is None: @@ -598,24 +608,12 @@ def main(argv: list[str] | None = None) -> int: report = cad_to_usd( args.source_asset, args.output_directory, - backend=args.backend, - usd_convert_cad_root=args.usd_convert_cad_root, - kit_app_template_root=args.kit_app_template_root, - kit_build_dir=args.kit_build_dir, - kit_executable=args.kit_executable, - cad_service_extension_dir=args.cad_service_extension_dir, - config_path=args.config_path, - execution_mode=args.execution_mode, + converter_python=args.usd_convert_cad_python, + skill_md=skill_md, output_extension=args.output_extension, - fine=args.fine, - coarse=args.coarse, - tessellation_chord=args.tessellation_chord, - tessellation_angle=args.tessellation_angle, - no_materials=args.no_materials, - single_mesh=args.single_mesh, - no_meter_units=args.no_meter_units, - keep_hidden=args.keep_hidden, timeout=args.timeout, + extra_args=passthrough, + legacy_backend=args.backend, ) emit_report(report, report_path=args.report, markdown_report_path=args.markdown_report) return 0 if report.passed else 1 diff --git a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-gsplat/README.md b/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-gsplat/README.md index 4d651daa..4ed98686 100644 --- a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-gsplat/README.md +++ b/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-gsplat/README.md @@ -10,11 +10,11 @@ The generated USD should be handed to `validate-usd-minimum` before any deeper v Use the upstream NVIDIA Omniverse `usd-convert-gsplat` skill as the authoritative reference for converter behavior, supported Gaussian splat fields, schema mapping, and converter-specific CLI/API options: -- Upstream skill: `https://github.com/NVIDIA-Omniverse/usd-convert-gsplat/blob/main/.agents/skills/usd-convert-gsplat/SKILL.md` +- Upstream skill: `https://github.com/NVIDIA-Omniverse/usd-convert-gsplat/blob/v0.1.15/.agents/skills/usd-convert-gsplat/SKILL.md` - Upstream repository: `https://github.com/NVIDIA-Omniverse/usd-convert-gsplat` - NVIDIA Omniverse gsplat-converter docs: `https://docs.omniverse.nvidia.com/kit/docs/gsplat-converter` -Access note: Browser or raw-file fetches of the upstream skill URL can fail when the repo requires GitHub credentials. If that happens, use an authenticated local clone of `https://github.com/NVIDIA-Omniverse/usd-convert-gsplat` and read `.agents/skills/usd-convert-gsplat/SKILL.md` from that checkout. Prefer `$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/usd-convert-gsplat` or `$HOME/.physical-ai-skill-hub/upstreams/usd-convert-gsplat`. +Access note: Browser or raw-file fetches of the upstream skill URL can fail when the repo requires GitHub credentials. If that happens, use an authenticated local clone of `https://github.com/NVIDIA-Omniverse/usd-convert-gsplat` checked out to the exact ref in `upstream-versions.lock.json` (currently `v0.1.15`) and read `.agents/skills/usd-convert-gsplat/SKILL.md` from that checkout. Prefer `$OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT/usd-convert-gsplat` or `$HOME/.omniverse-cad-to-simready/upstreams/usd-convert-gsplat`. Do not copy or reinterpret upstream conversion internals here. Keep this reference limited to this repo's wrapper contract, dependency check, report shape, and `validate-usd-minimum` handoff. @@ -31,7 +31,7 @@ Require: - external `gsplat2USD` CLI from `https://github.com/NVIDIA-Omniverse/gsplat-converter.git` - Python module `gsplat2USD` -The dependency is declared in this repo as a direct Git source for `gsplat2usd`. If the upstream repo is not accessible, `uv sync` will fail and the skill should report a blocked dependency. +The dependency is declared in this repo as a direct Git source for `gsplat2usd`, pinned to commit `c965f84bcbdc9667bb9631c6c922d12b4fea20ce` in both `pyproject.toml` and `upstream-versions.lock.json`. If the upstream repo is not accessible, `uv sync` will fail and the skill should report a blocked dependency. ## Conversion Workflow diff --git a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-gsplat/scripts/run.py b/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-gsplat/scripts/run.py index 3722a881..f41e5524 100644 --- a/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-gsplat/scripts/run.py +++ b/skills/omniverse-cad-to-simready/references/convert-to-usd/references/usd-convert-gsplat/scripts/run.py @@ -34,7 +34,7 @@ NEXT_STEP = "validate-usd-minimum" USD_OUTPUT_SUFFIXES = {".usd", ".usda", ".usdc", ".usdz"} UPSTREAM_REPO_URL = "https://github.com/NVIDIA-Omniverse/usd-convert-gsplat" -UPSTREAM_ROOT_ENV = "PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT" +UPSTREAM_ROOT_ENV = "OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT" @dataclass(frozen=True) @@ -120,7 +120,7 @@ def default_upstream_root() -> Path: root = os.environ.get(UPSTREAM_ROOT_ENV) if root: return Path(root).expanduser() / "usd-convert-gsplat" - return Path.home() / ".physical-ai-skill-hub" / "upstreams" / "usd-convert-gsplat" + return Path.home() / ".omniverse-cad-to-simready" / "upstreams" / "usd-convert-gsplat" def resolve_usd_convert_gsplat_root() -> Path: @@ -241,7 +241,7 @@ def convert_gsplat_to_usd( if suffixes is None: errors.append( "unable to read upstream usd-convert-gsplat supported formats; " - "set USD_CONVERT_GSPLAT_ROOT or PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT" + "set USD_CONVERT_GSPLAT_ROOT or OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT" ) elif source_asset.suffix.lower() not in suffixes: errors.append(f"unsupported Gaussian splat source format: {source_asset.suffix.lower() or 'unknown'}") diff --git a/skills/omniverse-cad-to-simready/references/deploy-content-agents/README.md b/skills/omniverse-cad-to-simready/references/deploy-content-agents/README.md index 2abcc78c..66f24dd4 100644 --- a/skills/omniverse-cad-to-simready/references/deploy-content-agents/README.md +++ b/skills/omniverse-cad-to-simready/references/deploy-content-agents/README.md @@ -8,24 +8,27 @@ This reference is documentation-driven, does not ship `scripts/run.py`, and shou ## Prerequisites -- NVIDIA_API_KEY from `https://build.nvidia.com` for provider-backed services. +- A provider API key for the selected Content Agents VLM/LLM backend. Use + `NVIDIA_API_KEY` from `https://build.nvidia.com` for NVIDIA-hosted defaults; + other hosted provider keys include `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, + `GOOGLE_API_KEY`, and `GEMINI_API_KEY`. - Docker, Docker Compose v2, NVIDIA Container Toolkit, an NVIDIA driver, and an NVIDIA GPU on the deployment host. -- A normalized upstream checkout of `https://github.com/nvidia-omniverse/content-agents` on branch `main`. +- A normalized upstream checkout of `https://github.com/nvidia-omniverse/content-agents` at the exact ref in `upstream-versions.lock.json` (currently `v0.5.2`). ## Upstream Reference -Use the NVIDIA Omniverse Content Agents `main` deployment skills as the source of truth: +Use the NVIDIA Omniverse Content Agents deployment skills at the tested manifest pin as the source of truth: | Target | Upstream skill | |---|---| -| Material Agent | `https://github.com/nvidia-omniverse/content-agents/blob/main/.codex/skills/deploy-material-agent-docker/SKILL.md` | -| Physics Agent | `https://github.com/nvidia-omniverse/content-agents/blob/main/.codex/skills/deploy-physics-agent-docker/SKILL.md` | -| Texture Agent | `https://github.com/nvidia-omniverse/content-agents/blob/main/.codex/skills/deploy-texture-agent-docker/SKILL.md` | -| OVRTX renderer | `https://github.com/nvidia-omniverse/content-agents/blob/main/.codex/skills/deploy-ovrtx-docker/SKILL.md` | +| Material Agent | `https://github.com/nvidia-omniverse/content-agents/blob/v0.5.2/.codex/skills/deploy-material-agent-docker/SKILL.md` | +| Physics Agent | `https://github.com/nvidia-omniverse/content-agents/blob/v0.5.2/.codex/skills/deploy-physics-agent-docker/SKILL.md` | +| Texture Agent | `https://github.com/nvidia-omniverse/content-agents/blob/v0.5.2/.codex/skills/deploy-texture-agent-docker/SKILL.md` | +| OVRTX renderer | `https://github.com/nvidia-omniverse/content-agents/blob/v0.5.2/.codex/skills/deploy-ovrtx-docker/SKILL.md` | -Repository: `https://github.com/nvidia-omniverse/content-agents`, branch `main` +Repository: `https://github.com/nvidia-omniverse/content-agents`, ref `v0.5.2` -If browser or raw-file fetches are blocked, use a local clone checked out to `main` and read `.codex/skills//SKILL.md` from that checkout. Resolve it from `CONTENT_AGENTS_UPSTREAM_ROOT`, then `$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/content-agents`, then `$HOME/.physical-ai-skill-hub/upstreams/content-agents`. Do not scan broad developer workspaces. +If browser or raw-file fetches are blocked, use a local clone checked out to the exact manifest ref and read `.codex/skills//SKILL.md` from that checkout. Resolve it from `CONTENT_AGENTS_UPSTREAM_ROOT`, then `$OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT/content-agents`, then `$HOME/.omniverse-cad-to-simready/upstreams/content-agents`. Do not scan broad developer workspaces. Do not duplicate full upstream deployment runbooks here. @@ -40,17 +43,20 @@ Collect: | `deployment_mode` | Local container deployment or reuse of healthy existing services. | | `render_endpoint` | Required for render-dependent agent services. | | `env_keys` | Provider/API keys required by the upstream skill. Never print or commit secrets. | +| `vlm_backend` / `vlm_model` | Optional deployment-time VLM selection, for example `openai` / `gpt-5.5`. | +| `llm_backend` / `llm_model` | Optional deployment-time LLM selection, for example `openai` / `gpt-5.5`. | ## Instructions 1. Resolve the upstream checkout from the normalized root policy. 2. Read the matching upstream deployment skill before issuing service-specific commands. 3. Confirm required secrets are available from local shell state or a private `.env`; if missing, ask the user and wait. -4. For `material`, `physics`, or `texture`, deploy or reuse OVRTX first when the selected upstream skill requires rendering. -5. Prefer a shared standalone OVRTX renderer plus independently deployed agent services for this workflow. Use bundled agent-specific renderer stacks only when the user asks for isolation or the upstream skill requires it. -6. Preserve upstream secret handling, build steps, image names, health checks, and service-specific environment details. -7. Verify the renderer and selected agent health endpoints before exporting `CONTENT_AGENTS_*_BASE_URL` or `RENDER_ENDPOINT`. -8. Return to the caller only after the requested service is healthy, or report the missing prerequisite as blocked. +4. Treat explicit endpoints as user-owned. Probe them and block if they are unhealthy; do not deploy a replacement container for that service. +5. For managed local deployment, deploy missing services in deterministic order: standalone OVRTX when Material or Physics will be deployed, then Material, Physics, and optional Texture one service at a time through the upstream collection deployment helper. +6. Prefer a shared standalone OVRTX renderer plus independently deployed agent services for this workflow. Use bundled agent-specific renderer stacks only when the user asks for isolation or the upstream skill requires it. +7. Preserve upstream secret handling, build steps, image names, health checks, and service-specific environment details. +8. Verify the renderer and selected agent health endpoints before exporting `CONTENT_AGENTS_*_BASE_URL` or `RENDER_ENDPOINT`. For local deployments, `RENDER_ENDPOINT` should point the agent containers at the standalone OVRTX HTTP service; upstream may describe this as the `remote` renderer backend because it is reached over HTTP. +9. Return to the caller only after the requested service is healthy, or report the missing prerequisite as blocked. ## Headless / Nested Host Notes @@ -63,10 +69,37 @@ service-specific environment variables. For local evaluation, prefer one shared standalone OVRTX renderer on the local GPU plus independent Material, Physics, and Texture Agent service containers -that use `NVIDIA_API_KEY` from `https://build.nvidia.com` for provider-backed -VLM calls. This topology does not require a separate local VLM GPU. Verify the -OVRTX host endpoint first, then verify each agent service reports configured API -keys before exporting `CONTENT_AGENTS_*_BASE_URL`. +that use a hosted provider key such as `NVIDIA_API_KEY`, `OPENAI_API_KEY`, +`ANTHROPIC_API_KEY`, `GOOGLE_API_KEY`, or `GEMINI_API_KEY` for provider-backed +VLM/LLM calls. This topology does not require a separate local VLM GPU. +Verify the OVRTX host endpoint first, then verify each agent service +reports configured API keys before exporting `CONTENT_AGENTS_*_BASE_URL`. +For managed local deployment, treat OVRTX as part of the Content Agents +deployment contract when Material or Physics will be deployed: start it first, +verify render health, then deploy each missing agent service individually. + +For hosted providers, use the upstream collection deployment path with +non-secret model overrides and keep keys in a private `.env`. Supported hosted +backends include `openai`, `anthropic`, and `gemini`. GPT-style OpenAI model +deployments should pin VLM/LLM temperatures to `1.0`, because some GPT models +only accept the default temperature: + +```bash +python3 skills/omniverse-cad-to-simready/references/preflight/scripts/preflight.py \ + --content-agents-secret-env-file "$HOME/Codes/.env" \ + --content-agents-vlm-backend openai \ + --content-agents-vlm-model gpt-5.5 \ + --content-agents-llm-backend openai \ + --content-agents-llm-model gpt-5.5 +``` + +For a custom OpenAI-compatible `/v1` VLM endpoint, set +`--content-agents-vlm-backend openai`, then provide +`--content-agents-vlm-endpoint` with the model ID served by that endpoint. +Use `--content-agents-vlm-api-key-env` when that endpoint should read a custom +secret name instead of the default `OPENAI_API_KEY`. Anthropic and Gemini use +their matching upstream key environment names. +Reserve `nim` for actual NVIDIA NIM endpoints. - Confirm Docker Compose v2 is installed before following the upstream deployment skills; the legacy `docker-compose` binary is not sufficient when @@ -90,6 +123,14 @@ keys before exporting `CONTENT_AGENTS_*_BASE_URL`. container health result and the host endpoint result before declaring the renderer blocked. +## Blocked Deployment Policy + +If deployment authentication is missing, ask the user for the matching +provider key and wait; if deployment cannot produce healthy services, report +Content Agents readiness as blocked instead of proceeding. See +`../preflight/README.md` and this reference for provider-key, dotenv, and +custom-endpoint detail. + ## Handoff Map | Target | After deployment | @@ -109,8 +150,8 @@ keys before exporting `CONTENT_AGENTS_*_BASE_URL`. | Symptom | Action | |---------|--------| -| Upstream skill URL cannot be fetched | Use the local `content-agents` clone checked out to `main`. | -| Required API key is missing | Ask the user for `NVIDIA_API_KEY` for deployment and wait. Usage tokens for already-running endpoints belong to the client references, not this deployment reference. | +| Upstream skill URL cannot be fetched | Use the local `content-agents` clone checked out to the exact ref in `upstream-versions.lock.json`. | +| Required API key is missing | Ask the user for the provider key matching the selected backend, such as `NVIDIA_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_API_KEY`, or `GEMINI_API_KEY`. Usage tokens for already-running endpoints belong to the client references, not this deployment reference. | | Service health is not ready | Follow the selected upstream deployment skill's health-check section. | | Renderer-dependent agent cannot reach OVRTX | Use the upstream renderer and agent deployment skills together; do not patch this repo with local Docker recipes. | | Nested host cannot start Docker containers | Check Docker Compose v2, GPU visibility, NVIDIA Container Toolkit, and whether the host requires the `vfs` storage-driver fallback. | diff --git a/skills/omniverse-cad-to-simready/references/deploy-content-agents/references/deploy-material-agent-docker.md b/skills/omniverse-cad-to-simready/references/deploy-content-agents/references/deploy-material-agent-docker.md index 432abb8e..01403249 100644 --- a/skills/omniverse-cad-to-simready/references/deploy-content-agents/references/deploy-material-agent-docker.md +++ b/skills/omniverse-cad-to-simready/references/deploy-content-agents/references/deploy-material-agent-docker.md @@ -1,8 +1,8 @@ # deploy-material-agent-docker Upstream source of truth: -`https://github.com/nvidia-omniverse/content-agents/blob/main/.codex/skills/deploy-material-agent-docker/SKILL.md`. +`https://github.com/nvidia-omniverse/content-agents/blob/v0.5.2/.codex/skills/deploy-material-agent-docker/SKILL.md`. Use the authenticated local clone at -`${CONTENT_AGENTS_UPSTREAM_ROOT:-${PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT:-$HOME/.physical-ai-skill-hub/upstreams}/content-agents}` +`${CONTENT_AGENTS_UPSTREAM_ROOT:-${OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT:-$HOME/.omniverse-cad-to-simready/upstreams}/content-agents}` checked out to `main` when browser or raw-file fetches are unavailable. diff --git a/skills/omniverse-cad-to-simready/references/deploy-content-agents/references/deploy-ovrtx-docker.md b/skills/omniverse-cad-to-simready/references/deploy-content-agents/references/deploy-ovrtx-docker.md index 37eec2f0..db8ed111 100644 --- a/skills/omniverse-cad-to-simready/references/deploy-content-agents/references/deploy-ovrtx-docker.md +++ b/skills/omniverse-cad-to-simready/references/deploy-content-agents/references/deploy-ovrtx-docker.md @@ -1,8 +1,8 @@ # deploy-ovrtx-docker Upstream source of truth: -`https://github.com/nvidia-omniverse/content-agents/blob/main/.codex/skills/deploy-ovrtx-docker/SKILL.md`. +`https://github.com/nvidia-omniverse/content-agents/blob/v0.5.2/.codex/skills/deploy-ovrtx-docker/SKILL.md`. Use the authenticated local clone at -`${CONTENT_AGENTS_UPSTREAM_ROOT:-${PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT:-$HOME/.physical-ai-skill-hub/upstreams}/content-agents}` +`${CONTENT_AGENTS_UPSTREAM_ROOT:-${OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT:-$HOME/.omniverse-cad-to-simready/upstreams}/content-agents}` checked out to `main` when browser or raw-file fetches are unavailable. diff --git a/skills/omniverse-cad-to-simready/references/deploy-content-agents/references/deploy-physics-agent-docker.md b/skills/omniverse-cad-to-simready/references/deploy-content-agents/references/deploy-physics-agent-docker.md index 1ddb0acb..11b9b73c 100644 --- a/skills/omniverse-cad-to-simready/references/deploy-content-agents/references/deploy-physics-agent-docker.md +++ b/skills/omniverse-cad-to-simready/references/deploy-content-agents/references/deploy-physics-agent-docker.md @@ -1,8 +1,8 @@ # deploy-physics-agent-docker Upstream source of truth: -`https://github.com/nvidia-omniverse/content-agents/blob/main/.codex/skills/deploy-physics-agent-docker/SKILL.md`. +`https://github.com/nvidia-omniverse/content-agents/blob/v0.5.2/.codex/skills/deploy-physics-agent-docker/SKILL.md`. Use the authenticated local clone at -`${CONTENT_AGENTS_UPSTREAM_ROOT:-${PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT:-$HOME/.physical-ai-skill-hub/upstreams}/content-agents}` +`${CONTENT_AGENTS_UPSTREAM_ROOT:-${OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT:-$HOME/.omniverse-cad-to-simready/upstreams}/content-agents}` checked out to `main` when browser or raw-file fetches are unavailable. diff --git a/skills/omniverse-cad-to-simready/references/deploy-content-agents/references/deploy-texture-agent-docker.md b/skills/omniverse-cad-to-simready/references/deploy-content-agents/references/deploy-texture-agent-docker.md index 705d2572..8895acb8 100644 --- a/skills/omniverse-cad-to-simready/references/deploy-content-agents/references/deploy-texture-agent-docker.md +++ b/skills/omniverse-cad-to-simready/references/deploy-content-agents/references/deploy-texture-agent-docker.md @@ -1,8 +1,8 @@ # deploy-texture-agent-docker Upstream source of truth: -`https://github.com/nvidia-omniverse/content-agents/blob/main/.codex/skills/deploy-texture-agent-docker/SKILL.md`. +`https://github.com/nvidia-omniverse/content-agents/blob/v0.5.2/.codex/skills/deploy-texture-agent-docker/SKILL.md`. Use the authenticated local clone at -`${CONTENT_AGENTS_UPSTREAM_ROOT:-${PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT:-$HOME/.physical-ai-skill-hub/upstreams}/content-agents}` +`${CONTENT_AGENTS_UPSTREAM_ROOT:-${OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT:-$HOME/.omniverse-cad-to-simready/upstreams}/content-agents}` checked out to `main` when browser or raw-file fetches are unavailable. diff --git a/skills/omniverse-cad-to-simready/references/nv-core-package-sample-validation/README.md b/skills/omniverse-cad-to-simready/references/nv-core-package-sample-validation/README.md index 7311b348..6192fb50 100644 --- a/skills/omniverse-cad-to-simready/references/nv-core-package-sample-validation/README.md +++ b/skills/omniverse-cad-to-simready/references/nv-core-package-sample-validation/README.md @@ -11,10 +11,10 @@ This reference does not convert source assets, repair USD layers, or publish pac The team reference workflow is NVIDIA's SimReady Foundation create-package skill: ```text -https://github.com/NVIDIA/simready-foundation/blob/main/skills/simready-foundation-create-package/SKILL.md +https://github.com/NVIDIA/simready-foundation/blob/v2026.04.1/skills/simready-foundation-create-package/SKILL.md ``` -Access note: Browser or raw-file fetches of the upstream skill URL can fail in restricted environments. If that happens, use a local clone of `https://github.com/NVIDIA/simready-foundation` on branch `main` and read `skills/simready-foundation-create-package/SKILL.md` from that checkout. Prefer `$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/simready-foundation` or `$HOME/.physical-ai-skill-hub/upstreams/simready-foundation`. +Access note: Browser or raw-file fetches of the upstream skill URL can fail in restricted environments. If that happens, use a local clone of `https://github.com/NVIDIA/simready-foundation` checked out to the exact ref in `upstream-versions.lock.json` (currently `v2026.04.1`) and read `skills/simready-foundation-create-package/SKILL.md` from that checkout. Prefer `$OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT/simready-foundation` or `$HOME/.omniverse-cad-to-simready/upstreams/simready-foundation`. For formal package post-validation with the registered SimReady Foundation package profiles, use the upstream `create_simready_package.py --only-post-validation` flow from that sample when `simready-validate` and WRAPP dependencies are installed. Do not duplicate or reinterpret the upstream package validation workflow inside this reference; point to the upstream skill and run the upstream sample when formal validation is required. This installed reference's `scripts/run.py` provides deterministic package checks for local package artifacts. diff --git a/skills/omniverse-cad-to-simready/references/nv-core-package-sample/README.md b/skills/omniverse-cad-to-simready/references/nv-core-package-sample/README.md index b2947a67..02fd1de7 100644 --- a/skills/omniverse-cad-to-simready/references/nv-core-package-sample/README.md +++ b/skills/omniverse-cad-to-simready/references/nv-core-package-sample/README.md @@ -23,10 +23,10 @@ The installed reference entrypoint is `scripts/run.py`. It creates `com.nvidia.s The team reference workflow is NVIDIA's SimReady Foundation create-package skill: ```text -https://github.com/NVIDIA/simready-foundation/blob/main/skills/simready-foundation-create-package/SKILL.md +https://github.com/NVIDIA/simready-foundation/blob/v2026.04.1/skills/simready-foundation-create-package/SKILL.md ``` -Access note: Browser or raw-file fetches of the upstream skill URL can fail in restricted environments. If that happens, use a local clone of `https://github.com/NVIDIA/simready-foundation` on branch `main` and read `skills/simready-foundation-create-package/SKILL.md` from that checkout. Prefer `$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/simready-foundation` or `$HOME/.physical-ai-skill-hub/upstreams/simready-foundation`. +Access note: Browser or raw-file fetches of the upstream skill URL can fail in restricted environments. If that happens, use a local clone of `https://github.com/NVIDIA/simready-foundation` checked out to the exact ref in `upstream-versions.lock.json` (currently `v2026.04.1`) and read `skills/simready-foundation-create-package/SKILL.md` from that checkout. Prefer `$OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT/simready-foundation` or `$HOME/.omniverse-cad-to-simready/upstreams/simready-foundation`. Use that upstream skill as the source of truth for the packaging flow. It drives `assets/scripts/create_simready_package.py`, which performs pre-validation, package creation, and post-validation in one command. Do not duplicate or reinterpret the upstream package workflow inside this reference; point to the upstream skill and run the upstream sample when formal packaging is required. In an installed reference, use `scripts/run.py --backend wrapp --upstream-scripts-dir /path/to/simready-foundation/skills/simready-foundation-create-package/assets/scripts` when the user has a local checkout and WRAPP runtime. @@ -85,7 +85,7 @@ WRAPP-backed publishing through the upstream sample: ```bash python3 /path/to/skills/omniverse-cad-to-simready/references/nv-core-package-sample/scripts/run.py /path/to/source \ --backend wrapp \ - --upstream-scripts-dir "$HOME/.physical-ai-skill-hub/upstreams/simready-foundation/skills/simready-foundation-create-package/assets/scripts" \ + --upstream-scripts-dir "$HOME/.omniverse-cad-to-simready/upstreams/simready-foundation/skills/simready-foundation-create-package/assets/scripts" \ --repo /path/to/repo \ --name apple_a01 \ --version 1.0.0 \ diff --git a/skills/omniverse-cad-to-simready/references/omni-asset-validate-geometry/scripts/check_dependencies.py b/skills/omniverse-cad-to-simready/references/omni-asset-validate-geometry/scripts/check_dependencies.py index c27203d9..a1a28702 100644 --- a/skills/omniverse-cad-to-simready/references/omni-asset-validate-geometry/scripts/check_dependencies.py +++ b/skills/omniverse-cad-to-simready/references/omni-asset-validate-geometry/scripts/check_dependencies.py @@ -16,6 +16,8 @@ from script_utils import check_result as _check, emit_json_report +from preflight_manifest import preflight_required, preflight_status_check + SKILL = "omni-asset-validate-geometry" TOOL = "omni_asset_validate" @@ -26,6 +28,12 @@ def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="Check portable geometry validation dependencies.") parser.add_argument("--report", type=Path) args = parser.parse_args(argv) + if preflight_required(): + preflight_check = preflight_status_check(SKILL, "asset_validator") + if not preflight_check["passed"]: + payload = {"skill": SKILL, "passed": False, "checks": [preflight_check], "errors": [preflight_check["message"]]} + emit_json_report(payload, args.report) + return 1 executable = shutil.which(TOOL) module_available = importlib.util.find_spec(MODULE_TOOL) is not None runtime = executable if executable is not None else (f"{sys.executable} -m {MODULE_TOOL}" if module_available else "not found") diff --git a/skills/omniverse-cad-to-simready/references/omni-asset-validate-physics/scripts/check_dependencies.py b/skills/omniverse-cad-to-simready/references/omni-asset-validate-physics/scripts/check_dependencies.py index 9b9d5344..2a8dd2e7 100644 --- a/skills/omniverse-cad-to-simready/references/omni-asset-validate-physics/scripts/check_dependencies.py +++ b/skills/omniverse-cad-to-simready/references/omni-asset-validate-physics/scripts/check_dependencies.py @@ -16,6 +16,8 @@ from script_utils import check_result as _check, emit_json_report +from preflight_manifest import preflight_required, preflight_status_check + SKILL = "omni-asset-validate-physics" TOOL = "omni_asset_validate" @@ -26,6 +28,12 @@ def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="Check portable physics validation dependencies.") parser.add_argument("--report", type=Path) args = parser.parse_args(argv) + if preflight_required(): + preflight_check = preflight_status_check(SKILL, "asset_validator") + if not preflight_check["passed"]: + payload = {"skill": SKILL, "passed": False, "checks": [preflight_check], "errors": [preflight_check["message"]]} + emit_json_report(payload, args.report) + return 1 executable = shutil.which(TOOL) module_available = importlib.util.find_spec(MODULE_TOOL) is not None runtime = executable if executable is not None else (f"{sys.executable} -m {MODULE_TOOL}" if module_available else "not found") diff --git a/skills/omniverse-cad-to-simready/references/omni-asset-validate/scripts/check_dependencies.py b/skills/omniverse-cad-to-simready/references/omni-asset-validate/scripts/check_dependencies.py index 8dbd7517..7d40b5ed 100644 --- a/skills/omniverse-cad-to-simready/references/omni-asset-validate/scripts/check_dependencies.py +++ b/skills/omniverse-cad-to-simready/references/omni-asset-validate/scripts/check_dependencies.py @@ -16,6 +16,8 @@ from script_utils import check_result as _check, emit_json_report +from preflight_manifest import preflight_required, preflight_status_check + SKILL = "omni-asset-validate" TOOL = "nvidia_usd_validate" @@ -29,6 +31,15 @@ def _write_report(payload: dict[str, Any], report_path: Path | None) -> None: def check_dependencies() -> dict[str, Any]: + if preflight_required(): + preflight_check = preflight_status_check(SKILL, "asset_validator") + if not preflight_check["passed"]: + return { + "skill": SKILL, + "passed": False, + "checks": [preflight_check], + "errors": [preflight_check["message"]], + } executable = shutil.which(TOOL) legacy_executable = shutil.which(LEGACY_TOOL) module_tool = next((name for name in (MODULE_TOOL, LEGACY_MODULE_TOOL) if importlib.util.find_spec(name) is not None), None) diff --git a/skills/omniverse-cad-to-simready/references/omni-asset-validate/scripts/run.py b/skills/omniverse-cad-to-simready/references/omni-asset-validate/scripts/run.py index 0ee69f1e..2cb8fc13 100644 --- a/skills/omniverse-cad-to-simready/references/omni-asset-validate/scripts/run.py +++ b/skills/omniverse-cad-to-simready/references/omni-asset-validate/scripts/run.py @@ -19,6 +19,8 @@ from script_utils import subprocess_output, tail_text +from preflight_manifest import preflight_required, preflight_status_check + SKILL = "omni-asset-validate" TOOL = "nvidia_usd_validate" @@ -226,6 +228,24 @@ def validate_with_asset_validator( asset_path = asset_path.resolve() categories = list(categories or []) rules = list(rules or []) + if preflight_required(): + preflight_check = preflight_status_check(SKILL, "asset_validator") + if not preflight_check["passed"]: + return AssetValidatorReport( + asset_path=str(asset_path), + validator_skill=SKILL, + validator_tool=TOOL, + passed=False, + status="BLOCKED", + command=[TOOL], + categories=categories, + rules=rules, + issue_counts={severity: 0 for severity in SEVERITIES}, + issues=[], + warnings=[], + errors=[preflight_check["message"]], + next_step=next_step, + ) command = [TOOL] for category in categories: command.extend(["--category", category]) diff --git a/skills/omniverse-cad-to-simready/references/ovrtx-render-service/README.md b/skills/omniverse-cad-to-simready/references/ovrtx-render-service/README.md index c84e25d6..19929506 100644 --- a/skills/omniverse-cad-to-simready/references/ovrtx-render-service/README.md +++ b/skills/omniverse-cad-to-simready/references/ovrtx-render-service/README.md @@ -28,9 +28,9 @@ the preflight guardrail. ## Upstream Reference -This reference follows the current `nvidia-omniverse/content-agents` `main` OVRTX rendering API contract: encode local USD as a data URI in the `/render` request `url` field, pass render settings, and decode returned image bytes from the response `images` map. Its stage-preparation behavior mirrors the useful parts of the earlier render-usd proof of concept from `https://github.com/NVIDIA-dev/content-claw/tree/main/.claude/skills/render-usd`: compute render bounds, author a camera from those bounds, preserve the source stage lighting state by default, and bundle local MDL/texture sidecars. The source asset is not modified. +This reference follows the `nvidia-omniverse/content-agents` OVRTX rendering API contract at the manifest pin `v0.5.2`: encode local USD as a data URI in the `/render` request `url` field, pass render settings, and decode returned image bytes from the response `images` map. Its stage-preparation behavior computes render bounds, authors a camera from those bounds, preserves the source stage lighting state by default, and bundles local MDL/texture sidecars. The source asset is not modified. -OVRTX service deployment belongs to NVIDIA Omniverse Content Agents: `https://github.com/nvidia-omniverse/content-agents` on branch `main`. When a render task requires provisioning, starting, or troubleshooting a local OVRTX render endpoint, use the installed `deploy-content-agents` skill, which points to `https://github.com/nvidia-omniverse/content-agents/blob/main/.codex/skills/deploy-ovrtx-docker/SKILL.md`. Do not copy Docker Compose or OVRTX deployment instructions into this reference. +OVRTX service deployment belongs to NVIDIA Omniverse Content Agents: `https://github.com/nvidia-omniverse/content-agents` at the exact ref in `upstream-versions.lock.json`. When a render task requires provisioning, starting, or troubleshooting a local OVRTX render endpoint, use the installed `deploy-content-agents` skill, which points to `https://github.com/nvidia-omniverse/content-agents/blob/v0.5.2/.codex/skills/deploy-ovrtx-docker/SKILL.md`. Do not copy Docker Compose or OVRTX deployment instructions into this reference. ## Stage Preparation @@ -51,9 +51,17 @@ when `pxr` is available: `--default-lights` only as an explicit debugging override. - Export a temporary `main.usda` and bundle referenced local MDL/texture files into the data-URI payload, rewriting asset paths to the bundle. +- When a material has a valid universal `UsdPreviewSurface` fallback, remove + `outputs:mdl:*` and `outputs:mtlx:*` from that temporary payload so OVRTX does + not choose an unsupported render-context shader and show a red error material. - Inspect the returned PNG for blank/uniform pixels and record the result in the - JSON report. Use `--fail-on-uniform` when a blank render should fail the - command rather than produce a warning. + JSON report. Pixel inspection requires Pillow (a pinned dependency of this + repo); a blank/uniform render, or an inspection that cannot run at all (for + example Pillow missing or the PNG unreadable), fails the stage by default so + packaging never proceeds on an unverified render. Pass + `--no-fail-on-uniform` only for diagnostic-only runs where a blank/uniform + render should stay a warning instead of a failure; a missing/unreadable + pixel inspection still fails the stage regardless of that flag. ## Camera Handling @@ -91,6 +99,7 @@ Require: - this reference's portable `scripts/run.py` and `scripts/check_dependencies.py`. - OpenUSD Python APIs through `pxr.Usd` and `pxr.UsdGeom` when local mesh statistics are required. Missing `pxr` is reported as a warning; the OVRTX render request can still proceed. +- Pillow (`PIL`) to inspect the rendered PNG for blank/uniform pixels. Pillow is a pinned dependency of this repo's environment (see the repo root `pyproject.toml`). Missing or non-functional Pillow fails the render stage; it is not an optional/best-effort check. - Python stdlib HTTP support for the OVRTX render service call. - For protected remote rendering, a bearer token from `OVRTX_RENDER_TOKEN`, `RENDER_TOKEN`, `CONTENT_AGENTS_RENDER_TOKEN`, `NGC_API_KEY`, @@ -145,7 +154,7 @@ required by this reference's normal `scripts/run.py` path. 4. Run this reference's portable `scripts/run.py` with the asset path and PNG output path. Keep default composition-preserving stage preparation enabled unless debugging a specific source-stage camera, light setup, or packaging issue. Do not pass `--flatten` for final report renders unless the unflattened render is blocked. Do not pass `--default-lights` for final report renders unless the user explicitly asks for authored lighting. 5. Preserve the JSON and Markdown reports when requested. 6. Confirm the output PNG exists and is non-empty. -7. Check that the output PNG is not blank or a uniform background image. If it is blank/uniform, mark the render as failed or blocked and troubleshoot the OVRTX request, camera, lighting, asset packaging, or endpoint; do not replace it with a Material Agent or Physics Agent report image. +7. Check that the output PNG is not blank or a uniform background image. `scripts/run.py` fails the stage by default when pixel inspection detects a blank/uniform PNG, or when pixel inspection could not run at all (for example Pillow missing); treat that failure as blocked and troubleshoot the OVRTX request, camera, lighting, asset packaging, or endpoint. Do not pass `--no-fail-on-uniform` and do not replace a blank render with a Material Agent or Physics Agent report image to force a pass. 8. If the single image is still blank or poorly framed, run `scripts/turntable.py` and inspect its frame reports before changing service endpoint assumptions. 9. Use the preview as diagnostic context for conversion, material, physics, SimReady, or package reports. diff --git a/skills/omniverse-cad-to-simready/references/ovrtx-render-service/scripts/check_dependencies.py b/skills/omniverse-cad-to-simready/references/ovrtx-render-service/scripts/check_dependencies.py index b7278c3a..e5569c45 100644 --- a/skills/omniverse-cad-to-simready/references/ovrtx-render-service/scripts/check_dependencies.py +++ b/skills/omniverse-cad-to-simready/references/ovrtx-render-service/scripts/check_dependencies.py @@ -53,6 +53,19 @@ def check_dependencies() -> dict[str, Any]: else: checks.append(_check("openusd_python_available", True, "OpenUSD Python modules are available", "info")) + try: + from PIL import Image, ImageStat # noqa: F401 + except Exception as exc: + checks.append( + _check( + "pillow_available", + False, + f"Pillow is unavailable: {exc}. Pixel inspection cannot run, so scripts/run.py will fail the render stage closed.", + ) + ) + else: + checks.append(_check("pillow_available", True, "Pillow is available for pixel inspection", "info")) + manifest, _, _ = load_preflight_manifest() endpoint = _env_first( ( diff --git a/skills/omniverse-cad-to-simready/references/ovrtx-render-service/scripts/run.py b/skills/omniverse-cad-to-simready/references/ovrtx-render-service/scripts/run.py index 7bc6b5ea..bb257a2a 100644 --- a/skills/omniverse-cad-to-simready/references/ovrtx-render-service/scripts/run.py +++ b/skills/omniverse-cad-to-simready/references/ovrtx-render-service/scripts/run.py @@ -19,11 +19,11 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "shared")) -from script_utils import check_result as _check, emit_json_report +from script_utils import check_result as _check, emit_json_report, inspect_png from preflight_manifest import load_preflight_manifest, preflight_required, preflight_status_check, ready_service_url -from stage_prep import can_prepare_with_openusd, inspect_png, prepare_render_stage, raw_asset_data_uri +from stage_prep import can_prepare_with_openusd, prepare_render_stage, raw_asset_data_uri SKILL = "ovrtx-render-service" @@ -208,7 +208,11 @@ def _collect_stage_stats(asset_path: Path) -> tuple[dict[str, Any] | None, str | mesh_count = 0 point_count = 0 triangle_count = 0 - for prim in stage.Traverse(): + # Read through instance proxies so externally referenced instanceable + # assets contribute their rendered meshes to the pre-render guard and + # scene statistics. This remains a read-only traversal; do not author + # through the returned proxy prims. + for prim in Usd.PrimRange.Stage(stage, Usd.TraverseInstanceProxies()): if not prim.IsA(UsdGeom.Mesh): continue mesh_count += 1 @@ -501,9 +505,12 @@ def render(args: argparse.Namespace) -> dict[str, Any]: pixel_inspection = inspect_png(output_image_path) report["pixel_inspection"] = pixel_inspection if pixel_inspection.get("available") is False: - warning = pixel_inspection.get("warning", "Could not inspect output PNG pixels") - report["warnings"].append(str(warning)) - checks.append(_check("output_png_pixel_inspected", False, str(warning), "warning")) + message = pixel_inspection.get("warning", "Could not inspect output PNG pixels") + # Pixel inspection is required to prove the render is not blank. An + # inspection failure (missing Pillow, unreadable PNG, ...) is a + # blocker, not a skip: we cannot tell a healthy render from a blank + # one, so this must fail closed rather than pass the stage. + checks.append(_check("output_png_pixel_inspected", False, str(message))) elif pixel_inspection.get("uniform"): message = "Output PNG is blank/uniform by pixel inspection" severity = "error" if args.fail_on_uniform else "warning" @@ -539,7 +546,16 @@ def _parser() -> argparse.ArgumentParser: parser.add_argument("--default-lights", action="store_true", help="Add default Dome/Sphere lights to lightless prepared stages.") parser.add_argument("--no-default-lights", action="store_true", help="Deprecated compatibility flag; default rendering does not author lights.") parser.add_argument("--no-bundle-local-assets", action="store_true", help="Do not bundle local MDL/texture assets referenced by the prepared stage.") - parser.add_argument("--fail-on-uniform", action="store_true", help="Return failure when the rendered PNG is blank or uniform.") + parser.add_argument( + "--fail-on-uniform", + action=argparse.BooleanOptionalAction, + default=True, + help=( + "Fail the stage when the rendered PNG is blank/uniform (default: on). " + "Pass --no-fail-on-uniform to demote a blank/uniform render to a warning " + "for diagnostic-only runs; packaging flows should keep this enabled." + ), + ) parser.add_argument("--request-timeout", type=int, default=120) parser.add_argument("--report", type=Path) parser.add_argument("--markdown-report", type=Path) diff --git a/skills/omniverse-cad-to-simready/references/ovrtx-render-service/scripts/stage_prep.py b/skills/omniverse-cad-to-simready/references/ovrtx-render-service/scripts/stage_prep.py index e72744b6..bf720683 100644 --- a/skills/omniverse-cad-to-simready/references/ovrtx-render-service/scripts/stage_prep.py +++ b/skills/omniverse-cad-to-simready/references/ovrtx-render-service/scripts/stage_prep.py @@ -65,7 +65,7 @@ def prepare_render_stage( bundle_local_assets: bool = True, force_generate_camera: bool = False, ) -> PreparedStage: - from pxr import Gf, Sdf, Usd, UsdGeom, UsdLux + from pxr import Gf, Sdf, Usd, UsdGeom, UsdLux, UsdShade asset_path = asset_path.resolve() warnings: list[str] = [] @@ -170,6 +170,19 @@ def prepare_render_stage( if bundle_local_assets: local_asset_count, copied_files = _bundle_local_assets(main_usda, asset_path.parent, Sdf) + stripped_render_context_outputs = _strip_render_context_outputs_for_preview_fallback( + main_usda, + Usd, + UsdShade, + ) + if stripped_render_context_outputs: + warnings.append( + "Stripped " + f"{stripped_render_context_outputs} render-context material output(s) " + "from the temporary OVRTX render payload where UsdPreviewSurface " + "fallbacks were available." + ) + if copied_files: archive = io.BytesIO() with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED) as bundle: @@ -202,6 +215,7 @@ def prepare_render_stage( "default_lights_added": lights_added, "local_asset_count": local_asset_count, "copied_local_assets": copied_files, + "render_context_outputs_stripped": stripped_render_context_outputs, "turntable_angle": turntable_angle, } return PreparedStage( @@ -454,6 +468,50 @@ def process_prim_spec(prim_spec: Any) -> int: return len(set(copied_files)), sorted(set(copied_files)) +def _material_has_preview_surface_output(material: Any, UsdShade: Any) -> bool: + surface_output = material.GetSurfaceOutput() + try: + connected_sources, _ = surface_output.GetConnectedSources() + except Exception: + return False + + for source_info in connected_sources: + try: + shader = UsdShade.Shader(source_info.source.GetPrim()) + if shader.GetIdAttr().Get() == "UsdPreviewSurface": + return True + except Exception: + continue + return False + + +def _strip_render_context_outputs_for_preview_fallback( + stage_path: Path, + Usd: Any, + UsdShade: Any, +) -> int: + stage = Usd.Stage.Open(str(stage_path)) + if stage is None: + return 0 + + removed = 0 + for prim in stage.Traverse(): + if not prim.IsA(UsdShade.Material): + continue + material = UsdShade.Material(prim) + if not _material_has_preview_surface_output(material, UsdShade): + continue + for prop in list(prim.GetProperties()): + prop_name = prop.GetName() + if prop_name.startswith(("outputs:mdl:", "outputs:mtlx:")): + prim.RemoveProperty(prop_name) + removed += 1 + + if removed: + stage.GetRootLayer().Save() + return removed + + def _json_bounds(bounds_info: dict[str, Any]) -> dict[str, Any]: return { "empty": bool(bounds_info.get("empty")), @@ -463,30 +521,3 @@ def _json_bounds(bounds_info: dict[str, Any]) -> dict[str, Any]: "center": [float(v) for v in bounds_info.get("center", [])], "radius": float(bounds_info.get("radius", 0.0)), } - - -def inspect_png(path: Path) -> dict[str, Any]: - try: - from PIL import Image, ImageStat - except Exception as exc: - return {"available": False, "warning": f"Pillow is unavailable: {exc}"} - - try: - image = Image.open(path).convert("RGB") - except Exception as exc: - return {"available": False, "warning": f"Could not inspect PNG pixels: {exc}"} - small = image.resize((min(64, image.width), min(64, image.height))) - pixels = small.get_flattened_data() if hasattr(small, "get_flattened_data") else small.getdata() - unique = len(set(pixels)) - extrema = image.getextrema() - uniform = unique <= 1 or all(low == high for low, high in extrema) - all_black = all(high == 0 for _, high in extrema) - return { - "available": True, - "size": [image.width, image.height], - "extrema": [[int(low), int(high)] for low, high in extrema], - "unique_colors_after_resize": unique, - "channel_mean": [float(v) for v in ImageStat.Stat(image).mean], - "uniform": bool(uniform), - "all_black": bool(all_black), - } diff --git a/skills/omniverse-cad-to-simready/references/ovrtx-render-service/scripts/turntable.py b/skills/omniverse-cad-to-simready/references/ovrtx-render-service/scripts/turntable.py index dd43bbaf..1f16a739 100644 --- a/skills/omniverse-cad-to-simready/references/ovrtx-render-service/scripts/turntable.py +++ b/skills/omniverse-cad-to-simready/references/ovrtx-render-service/scripts/turntable.py @@ -10,8 +10,8 @@ from typing import Any from run import _check, _decode_png, _endpoint_kind, _endpoint_requires_token, _post_json, _resolve_endpoint, _resolve_token -from script_utils import emit_json_report -from stage_prep import inspect_png, prepare_render_stage +from script_utils import emit_json_report, inspect_png +from stage_prep import prepare_render_stage SKILL = "ovrtx-render-service" diff --git a/skills/omniverse-cad-to-simready/references/preflight/README.md b/skills/omniverse-cad-to-simready/references/preflight/README.md index e677a4b0..89f5b838 100644 --- a/skills/omniverse-cad-to-simready/references/preflight/README.md +++ b/skills/omniverse-cad-to-simready/references/preflight/README.md @@ -21,8 +21,10 @@ assignment, conformance, validation, rendering, or packaging on an asset. - `git`, and `git-lfs` when LFS fixtures or source assets must be materialized. - Network and repository access for the upstream sources listed below. - Docker, Docker Compose v2, NVIDIA Container Toolkit, an NVIDIA driver, an - NVIDIA GPU, and `NVIDIA_API_KEY` when managed local Content Agents deployment - is requested. + NVIDIA GPU, and at least one configured Content Agents model provider key + when managed local Content Agents deployment is requested. Supported provider + keys include `NVIDIA_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, + `GOOGLE_API_KEY`, and `GEMINI_API_KEY`. Windows hosts can use the same Python preflight script and PowerShell wrapper for checkout and Python-runtime preparation. Managed Content Agents deployment @@ -32,40 +34,49 @@ healthy service endpoints. ## Upstream Sources The preflight installs or verifies local checkouts under -`${PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT:-$HOME/.physical-ai-skill-hub/upstreams}` +`${OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT:-$HOME/.omniverse-cad-to-simready/upstreams}` unless a per-upstream override is set. -| Area | Upstream | Default checkout / override | -|---|---|---| -| CAD conversion | `https://github.com/NVIDIA-Omniverse/usd-convert-cad` | `usd-convert-cad`, `USD_CONVERT_CAD_ROOT` | -| Gaussian splat conversion | `https://github.com/NVIDIA-Omniverse/usd-convert-gsplat` | `usd-convert-gsplat`, `USD_CONVERT_GSPLAT_ROOT` | -| SimReady validation and FET skills | `https://github.com/NVIDIA/simready-foundation` on branch `main` | `simready-foundation`, `SIMREADY_FOUNDATION_ROOT` | -| Content Agents services | `https://github.com/nvidia-omniverse/content-agents` on branch `main` | `content-agents`, `CONTENT_AGENTS_UPSTREAM_ROOT` | +| Area | Upstream | Pinned ref | Default checkout / override | +|---|---|---|---| +| CAD conversion guidance | `https://github.com/NVIDIA-Omniverse/usd-convert-cad` | `v0.2.0` | `usd-convert-cad`, `USD_CONVERT_CAD_ROOT` | +| Gaussian splat conversion | `https://github.com/NVIDIA-Omniverse/usd-convert-gsplat` | `v0.1.15` | `usd-convert-gsplat`, `USD_CONVERT_GSPLAT_ROOT` | +| SimReady validation and FET skills | `https://github.com/NVIDIA/simready-foundation` | `v2026.04.1` | `simready-foundation`, `SIMREADY_FOUNDATION_ROOT` | +| Content Agents services | `https://github.com/nvidia-omniverse/content-agents` | `v0.5.2` | `content-agents`, `CONTENT_AGENTS_UPSTREAM_ROOT` | The upstream URLs remain documented because they are the source of truth for external NVIDIA technology. Operationally, downstream references should prefer the preflight manifest when it is present. +`upstream-versions.lock.json` is the single tested-version manifest. It pins +each Git checkout to both a human-readable tag and an immutable commit, and it +pins the CAD and SimReady Python runtimes to exact direct and transitive package +versions. Preflight checks out managed clones at those commits and never pulls +`main`. Per-upstream overrides are not mutated; they are accepted only when +their current commit matches the manifest. A mismatch blocks with a clear +`requires , found ` message. `--check-only` and `--no-update` also +verify the pin without changing the checkout. + ## CLI Pattern Linux/macOS: ```bash .agents/skills/omniverse-cad-to-simready/references/preflight/scripts/preflight.sh \ - --env-file "$HOME/.physical-ai-skill-hub/state/cad-to-simready-preflight.env" \ - --markdown-report "$HOME/.physical-ai-skill-hub/state/cad-to-simready-preflight.md" + --env-file "$HOME/.omniverse-cad-to-simready/state/cad-to-simready-preflight.env" \ + --markdown-report "$HOME/.omniverse-cad-to-simready/state/cad-to-simready-preflight.md" -. "$HOME/.physical-ai-skill-hub/state/cad-to-simready-preflight.env" +. "$HOME/.omniverse-cad-to-simready/state/cad-to-simready-preflight.env" ``` Windows PowerShell: ```powershell .\.agents\skills\omniverse-cad-to-simready\references\preflight\scripts\preflight.ps1 ` - --powershell-env-file "$HOME\.physical-ai-skill-hub\state\cad-to-simready-preflight.ps1" ` - --markdown-report "$HOME\.physical-ai-skill-hub\state\cad-to-simready-preflight.md" + --powershell-env-file "$HOME\.omniverse-cad-to-simready\state\cad-to-simready-preflight.ps1" ` + --markdown-report "$HOME\.omniverse-cad-to-simready\state\cad-to-simready-preflight.md" -. "$HOME\.physical-ai-skill-hub\state\cad-to-simready-preflight.ps1" +. "$HOME\.omniverse-cad-to-simready\state\cad-to-simready-preflight.ps1" ``` Dependency bootstrap without Content Agents service deployment: @@ -73,9 +84,28 @@ Dependency bootstrap without Content Agents service deployment: ```bash python3 .agents/skills/omniverse-cad-to-simready/references/preflight/scripts/preflight.py \ --skip-content-agents \ - --env-file "$HOME/.physical-ai-skill-hub/state/cad-to-simready-preflight.env" + --env-file "$HOME/.omniverse-cad-to-simready/state/cad-to-simready-preflight.env" +``` + +Managed Content Agents deployment can read OpenAI, Anthropic, or Google/Gemini +credentials from a private dotenv file without writing secrets to the manifest: + +```bash +python3 .agents/skills/omniverse-cad-to-simready/references/preflight/scripts/preflight.py \ + --content-agents-secret-env-file "$HOME/Codes/.env" \ + --content-agents-vlm-backend openai \ + --content-agents-vlm-model gpt-5.5 \ + --content-agents-llm-backend openai \ + --content-agents-llm-model gpt-5.5 \ + --env-file "$HOME/.omniverse-cad-to-simready/state/cad-to-simready-preflight.env" ``` +Use `--content-agents-vlm-backend anthropic` with a Claude VLM model, or +`--content-agents-vlm-backend gemini` with a Gemini VLM model, when those keys +are present in the private dotenv. For a custom OpenAI-compatible `/v1` VLM +endpoint, use `--content-agents-vlm-backend openai` and pass +`--content-agents-vlm-endpoint`; reserve `nim` for actual NIM endpoints. + Read-only readiness check: ```bash @@ -89,7 +119,7 @@ python3 .agents/skills/omniverse-cad-to-simready/references/preflight/scripts/pr The default manifest path is: ```text -${PHYSICAL_AI_SKILL_HUB_STATE:-$HOME/.physical-ai-skill-hub/state}/cad-to-simready-preflight.json +${OMNIVERSE_CAD_TO_SIMREADY_STATE:-$HOME/.omniverse-cad-to-simready/state}/cad-to-simready-preflight.json ``` Set `PHYSICAL_AI_PREFLIGHT_MANIFEST` to point downstream references at a @@ -101,8 +131,8 @@ The generated env file exports: - `PHYSICAL_AI_PREFLIGHT_MANIFEST` - `PHYSICAL_AI_REQUIRE_PREFLIGHT=1` -- `PHYSICAL_AI_SKILL_HUB_HOME` -- `PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT` +- `OMNIVERSE_CAD_TO_SIMREADY_HOME` +- `OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT` - `PATH` with the repository `.venv/bin` prepended when the project virtual environment is present, so direct reference scripts can find bundled CLIs such as `urdf_usd_converter` @@ -115,35 +145,88 @@ Use `--env-file` for POSIX shells and `--powershell-env-file` for PowerShell. The manifest never writes API keys, bearer tokens, or file-backed secret contents. Command output is redacted before it is included in the report. +It records `upstream_version_policy: pinned-tested-integration` and the path to +the version manifest so QA evidence identifies the exact dependency set. ## Content Agents Policy -Content Agents readiness is included by default. Preflight first reuses healthy -existing `CONTENT_AGENTS_*_BASE_URL` and renderer endpoints. Material, -Physics, and Texture endpoints must also report configured API keys when their -health payload includes `api_keys_configured`; a healthy container without -service credentials is not workflow-ready. If endpoints are not ready and -deployment is enabled, preflight checks Docker/GPU/auth prerequisites with -`nvidia-smi`, the Docker daemon, Docker Compose v2, and `NVIDIA_API_KEY`, then -invokes executable upstream deployment entrypoints only when the -`content-agents` checkout publishes them. The upstream collection helper -starts agent services; when the shared OVRTX endpoint is still unhealthy after -that step, preflight invokes the upstream standalone OVRTX Docker Compose -entrypoint from `apps/ovrtx_rendering_api/docker-compose.yml` and waits for the -host `/health` endpoint. +Content Agents readiness is included by default. Preflight treats explicitly +provided `CONTENT_AGENTS_*_BASE_URL` and renderer endpoints as user-owned: it +probes them and blocks when they are unhealthy, but does not replace them with +managed containers. For services without explicit endpoints, preflight deploys +the managed local topology in deterministic order: standalone OVRTX first when +Material or Physics will be deployed, then Material, Physics, and optional +Texture one service at a time through the upstream collection deployment helper. +Material, Physics, and Texture endpoints must also report configured API keys +when their health payload includes `api_keys_configured`; a healthy container +without service credentials is not workflow-ready, except for the provided +already-running endpoints described under the render-key mirror below. Before +deploying, preflight checks Docker/GPU/auth prerequisites with `nvidia-smi`, the +Docker daemon, Docker Compose v2, and a configured provider credential. Use +`--content-agents-secret-env-file` to load credentials such as `OPENAI_API_KEY`, +`ANTHROPIC_API_KEY`, `GOOGLE_API_KEY`, or `GEMINI_API_KEY` from a private dotenv +file for deployment only. Use `--content-agents-vlm-*` and +`--content-agents-llm-*` to select hosted (`openai`, `anthropic`, `gemini`), +custom OpenAI-compatible (`openai` plus endpoint), or NIM (`nim` plus endpoint) +VLM/LLM backends and model IDs. Generated collection configs use the selected +backend to choose the default secret environment variable name: +`OPENAI_API_KEY` for `openai`, `ANTHROPIC_API_KEY` for `anthropic`, +and `GOOGLE_API_KEY` or `GEMINI_API_KEY` for `gemini`. Use +`--content-agents-vlm-api-key-env` and `--content-agents-llm-api-key-env` when +the endpoint or provider should read a custom secret name. NIM endpoints remain +NIM-specific and do not use these generic `*_API_KEY_ENV` settings. +For GPT-style OpenAI models, generated deployment config pins +VLM/LLM temperatures to `1.0`, matching models that only accept the default +temperature. The upstream agent collection uses the OVRTX HTTP endpoint through +`RENDER_ENDPOINT`; upstream logs may call this the `remote` renderer because the +renderer is an HTTP service, not because NVCF was selected. For managed local deployment, known Content Agents credential environment -variables such as `NVIDIA_API_KEY` are mirrored into the upstream checkout's +variables for the selected backend are mirrored into the upstream checkout's private `.env` file with owner-only permissions so Docker Compose can pass them to containers. Those values are not written to the preflight manifest or -generated downstream env file. When `NGC_API_KEY` is absent, the managed local -deployment mirrors `NVIDIA_API_KEY` into that name inside the upstream `.env` -because the upstream collection's local render endpoint is reached from -containers through a Docker host alias. - -For remote or NVCF-style endpoints, preflight records the provided endpoint as -ready without treating generic unauthenticated `/health` failures as blockers. -The selected service wrapper still performs the authenticated service call and -reports any real request failure. +generated downstream env file. When `NGC_API_KEY` is absent and the render +endpoint is local, the managed local deployment mirrors the first configured +provider credential -- `NVIDIA_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, +`GOOGLE_API_KEY`, `GEMINI_API_KEY`, or another known Content Agents provider +name -- into `NGC_API_KEY` inside the upstream `.env`. Upstream Material and +Physics readiness demands a render usage key for any render host it does not +recognize as local, and a host-local renderer is reached from containers through +the `host.docker.internal` Docker host alias, so that mirror keeps a local +renderer from being treated as remote. `localhost`, `*.localhost`, loopback +addresses, unspecified addresses such as `0.0.0.0`, and `host.docker.internal` +all count as local. When the render endpoint is remote, no provider credential +is mirrored; that endpoint needs a real render usage key in `NGC_API_KEY` or +`NVCF_API_KEY`, and preflight names that requirement when an agent reports +`api_keys_configured: false`. + +Preflight cannot mirror that credential into an agent it did not deploy. So for +an explicitly provided Material, Physics, or Texture endpoint that is already +running, `api_keys_configured: false` is accepted as ready when the configured +render endpoint is on this machine under a name upstream readiness does not +recognize as local, such as the `host.docker.internal` Docker host alias. The +upstream allowlist gap is enough to explain that combination, and the renderer +still serves over HTTP. The unmodified health payload stays in the report. This +applies only to that gap: a provided endpoint reporting +`api_keys_configured: false` against a renderer upstream already accepts, such +as `localhost` or a loopback address, still blocks, because only a VLM or LLM +credential can explain it there. A remote render endpoint also still blocks and +still needs a real render usage key. + +This acceptance is deliberately a false-accept risk, not a proof. Upstream +`has_required_api_keys` returns `vlm_ready and render_ready`, so a failing render +gate masks any VLM or LLM credential gap present at the same time, and `/health` +exposes only the aggregate flag. Preflight cannot tell the two apart, so a +provided agent behind the Docker host alias that is *also* missing a real VLM or +LLM credential now passes this gate and fails later at call time, with the +service error surfaced then. Physics narrows further: it evaluates the render +gate only while `PA_RENDER_BACKEND` is `remote` (its default), so if that is set +to anything else, `api_keys_configured: false` from a Physics agent is always a +credential gap and never the allowlist. + +For remote or NVCF-style endpoints that a user explicitly provides, preflight +records the provided endpoint as ready without treating generic unauthenticated +`/health` failures as blockers. Local OVRTX endpoints are probed again by the +selected service wrapper immediately before Material or Physics Agent sessions. Do not encode service-specific Docker Compose files, image names, ports inside containers, or deployment runbooks in this repo. If the selected upstream @@ -168,11 +251,11 @@ require `usd-convert-cad`; Gaussian splat routes require `usd-convert-gsplat`. Validation targets also gate OpenUSD Python APIs (`pxr.Usd`, `pxr.UsdGeom`, and `pxr.UsdPhysics`) and the upstream Asset Validator runtime (`omni_asset_validate` or `omni.asset_validator`) before SimReady validation. -On Linux aarch64, if the SimReady Foundation requirements cannot resolve -PyPI `usd-core`, preflight retries the SimReady validation runtime with -`usd-exchange>=2.3.0`, `omniverse-asset-validator`, -`omniverse-usd-profiles`, non-`usd-core` Foundation requirements, and -`simready-validate` installed without dependencies. +On every platform, preflight builds the SimReady validation venv from the exact +package set in `upstream-versions.lock.json`, using `usd-exchange==2.3.0` as the +OpenUSD runtime and installing `simready-validate==2026.4.8` without its +unavailable-on-aarch64 `usd-core` dependency. It then rereads all installed +distribution versions and blocks if any direct or transitive pin differs. If `uv` is missing, the `repo_python` runtime entry includes an install hint: `curl -LsSf https://astral.sh/uv/install.sh | sh`. diff --git a/skills/omniverse-cad-to-simready/references/preflight/scripts/preflight.py b/skills/omniverse-cad-to-simready/references/preflight/scripts/preflight.py index 2e6460b7..1da2bd75 100755 --- a/skills/omniverse-cad-to-simready/references/preflight/scripts/preflight.py +++ b/skills/omniverse-cad-to-simready/references/preflight/scripts/preflight.py @@ -30,10 +30,12 @@ import base64 from dataclasses import dataclass, field from datetime import datetime, timezone +import ipaddress import json import os from pathlib import Path import platform +import re import shutil import subprocess import sys @@ -43,10 +45,22 @@ from urllib.parse import urlparse, urlunparse from urllib.request import Request, urlopen +import yaml + sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "shared")) from script_utils import tail_text -from usd_convert_cad_diagnostics import summarize_usd_convert_cad_validation_failure +from upstream_versions import ( + UPSTREAM_VERSION_MANIFEST_PATH, + expected_versions, + git_upstreams, + package_requirement, + package_version, + runtime_package_names, + runtime_requirements, + version_mismatches, + version_probe_code, +) SKILL = "cad-to-simready-preflight" @@ -113,28 +127,79 @@ def DistantLight "KeyLight" "NVIDIA_API_KEY", "NGC_API_KEY", "NVCF_API_KEY", - "NSTORAGE_API_KEY", - "INFERENCE_NVIDIA_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GOOGLE_API_KEY", "GEMINI_API_KEY", + "WU_NIM_API_KEY", "MA_NVIDIA_API_KEY", - "MA_NSTORAGE_API_KEY", "MA_IMAGE_GEN_API_KEY", "MA_CLUSTER_EMBEDDING_API_KEY", "MA_NIM_API_KEY", "PA_NVIDIA_API_KEY", - "PA_NSTORAGE_API_KEY", "PA_NIM_API_KEY", "TA_IMAGE_GEN_API_KEY", ) -SIMREADY_RUNTIME_EXTRA_REQUIREMENTS = ("numpy>=1.24,<3",) -DEFAULT_SIMREADY_VALIDATE_REQUIREMENT = "simready-validate>=2026.4.8" -USD_EXCHANGE_SDK_FALLBACK_REQUIREMENTS = ( - "usd-exchange>=2.3.0", - "omniverse-asset-validator", - "omniverse-usd-profiles>=1.10.22", +CONTENT_AGENTS_PROVIDER_SECRET_ENV_NAMES = ( + "NVIDIA_API_KEY", + "NGC_API_KEY", + "OPENAI_API_KEY", + "ANTHROPIC_API_KEY", + "GOOGLE_API_KEY", + "GEMINI_API_KEY", + "WU_NIM_API_KEY", + "MA_NVIDIA_API_KEY", + "MA_NIM_API_KEY", + "PA_NVIDIA_API_KEY", + "PA_NIM_API_KEY", + "TA_IMAGE_GEN_API_KEY", +) +CONTENT_AGENTS_BACKEND_API_KEY_ENV_NAMES = { + "nim": ("NVIDIA_API_KEY", "WU_NIM_API_KEY", "MA_NIM_API_KEY", "PA_NIM_API_KEY"), + "openai": ("OPENAI_API_KEY",), + "anthropic": ("ANTHROPIC_API_KEY",), + "gemini": ("GOOGLE_API_KEY", "GEMINI_API_KEY"), +} +CONTENT_AGENTS_DEPENDENCY_API_KEY_ENV_NAMES = { + "image_gen": ("MA_IMAGE_GEN_API_KEY", "TA_IMAGE_GEN_API_KEY"), +} +CONTENT_AGENTS_MODEL_ENV_NAMES = ( + "CONTENT_AGENTS_VLM_BACKEND", + "CONTENT_AGENTS_VLM_MODEL", + "CONTENT_AGENTS_VLM_ENDPOINT", + "CONTENT_AGENTS_VLM_API_KEY_ENV", + "MA_VLM_API_KEY_ENV", + "PA_VLM_API_KEY_ENV", + "CONTENT_AGENTS_LLM_BACKEND", + "CONTENT_AGENTS_LLM_MODEL", + "CONTENT_AGENTS_LLM_ENDPOINT", + "CONTENT_AGENTS_LLM_API_KEY_ENV", + "MA_LLM_API_KEY_ENV", + "TA_LLM_API_KEY_ENV", + "CONTENT_AGENTS_IMAGE_GEN_BACKEND", + "CONTENT_AGENTS_IMAGE_GEN_MODEL", + "CONTENT_AGENTS_IMAGE_GEN_ENDPOINT", + "CONTENT_AGENTS_IMAGE_GEN_API_KEY_ENV", + "MA_IMAGE_GEN_API_KEY_ENV", + "TA_IMAGE_GEN_API_KEY_ENV", +) +CONTENT_AGENTS_API_KEY_ENV_NAME_ENV_NAMES = ( + "CONTENT_AGENTS_VLM_API_KEY_ENV", + "MA_VLM_API_KEY_ENV", + "PA_VLM_API_KEY_ENV", + "CONTENT_AGENTS_LLM_API_KEY_ENV", + "MA_LLM_API_KEY_ENV", + "TA_LLM_API_KEY_ENV", + "CONTENT_AGENTS_IMAGE_GEN_API_KEY_ENV", + "MA_IMAGE_GEN_API_KEY_ENV", + "TA_IMAGE_GEN_API_KEY_ENV", +) +CREDENTIAL_ENV_NAME_PATTERN = re.compile(r"^[A-Z_][A-Z0-9_]*$") +SIMREADY_RUNTIME_REQUIREMENTS = runtime_requirements("simready_validate") +SIMREADY_NO_DEPS_REQUIREMENTS = runtime_requirements("simready_validate", no_deps=True) +SIMREADY_RUNTIME_PACKAGE_NAMES = ( + *runtime_package_names("simready_validate"), + *runtime_package_names("simready_validate", no_deps=True), ) DEFAULT_CONVERSION_TOOLS = { "repo-python", @@ -143,18 +208,41 @@ def DistantLight "KeyLight" } UV_INSTALL_HINT = "Install uv with: curl -LsSf https://astral.sh/uv/install.sh | sh" OPENUSD_IMPORT_CHECK = "from pxr import Usd, UsdGeom, UsdPhysics; print(Usd.GetVersion())" +USD_CONVERT_CAD_PYTHON_ENV = "USD_CONVERT_CAD_PYTHON" +USD_CONVERT_CAD_PIP_PACKAGE = package_requirement("usd-convert-cad") +USD_CONVERT_CAD_VERSION = package_version("usd-convert-cad") +USD_CONVERT_CAD_IMPORT_CHECK = ( + "import usd_convert_cad as m; " + "print(getattr(m, '__version__', None) or " + "(m.get_version() if hasattr(m, 'get_version') else 'unknown'))" +) +USD_CONVERT_CAD_INSTALL_HINT = ( + "usd-convert-cad 0.2 is a self-contained wheel. Install it into an isolated Python 3.12 " + f"environment and expose that interpreter via {USD_CONVERT_CAD_PYTHON_ENV}: " + 'python3.12 -m venv "$HOME/.omniverse-cad-to-simready/venvs/usd-convert-cad" ' + '&& "$HOME/.omniverse-cad-to-simready/venvs/usd-convert-cad/bin/python" -m pip install ' + f"{USD_CONVERT_CAD_PIP_PACKAGE} --extra-index-url https://pypi.nvidia.com " + f'&& export {USD_CONVERT_CAD_PYTHON_ENV}="$HOME/.omniverse-cad-to-simready/venvs/usd-convert-cad/bin/python"' +) ASSET_VALIDATOR_IMPORT_CHECK = "import omni.asset_validator" USD_SUFFIXES = {".usd", ".usda", ".usdc", ".usdz"} REPO_PYTHON_SOURCE_FORMATS = {"urdf", "mjcf", "mujoco"} +# Source suffixes usd-convert-cad advertises in its Supported Formats table +# (`.agents/skills/usd-convert-cad/SKILL.md`). `.ply` intentionally routes to +# usd-convert-gsplat, which is checked ahead of CAD in `_normalized_source_format`. CAD_SOURCE_SUFFIXES = { ".3dm", ".3ds", + ".3dxml", ".3mf", ".asm", ".catpart", ".catproduct", + ".cgr", ".dae", ".dgn", + ".dwg", + ".dxf", ".fbx", ".glb", ".gltf", @@ -166,44 +254,60 @@ def DistantLight "KeyLight" ".ipt", ".jt", ".obj", - ".ply", + ".par", ".prt", + ".psm", + ".pwd", + ".rfa", + ".rvt", + ".sab", + ".sat", ".sldasm", ".sldprt", ".step", ".stl", ".stp", + ".x_b", ".x_t", + ".xmt", + ".xmt_txt", } GSPLAT_SOURCE_SUFFIXES = {".ply", ".splat", ".ksplat"} +DOCKER_HOST_ALIAS = "host.docker.internal" +# Hosts that name the local machine from outside a container and therefore have +# to be rewritten to the Docker host alias before an agent container can reach +# them. LOCAL_RENDER_HOSTS = {"localhost", "127.0.0.1", "::1", "0.0.0.0"} -CONTAINER_RENDER_ENV_KEYS = ("RENDER_ENDPOINT", "OVRTX_RENDER_ENDPOINT", "CONTENT_AGENTS_RENDER_BASE_URL") -UPSTREAMS = { - "usd_convert_cad": { - "url": "https://github.com/NVIDIA-Omniverse/usd-convert-cad", - "branch": None, - "checkout": "usd-convert-cad", - "env": "USD_CONVERT_CAD_ROOT", - }, - "usd_convert_gsplat": { - "url": "https://github.com/NVIDIA-Omniverse/usd-convert-gsplat", - "branch": None, - "checkout": "usd-convert-gsplat", - "env": "USD_CONVERT_GSPLAT_ROOT", - }, - "simready_foundation": { - "url": "https://github.com/NVIDIA/simready-foundation", - "branch": "main", - "checkout": "simready-foundation", - "env": "SIMREADY_FOUNDATION_ROOT", - }, - "content_agents": { - "url": "https://github.com/nvidia-omniverse/content-agents", - "branch": "main", - "checkout": "content-agents", - "env": "CONTENT_AGENTS_UPSTREAM_ROOT", - }, +# Hosts that mean "the renderer runs on this machine", including the Docker host +# alias, which is already container-reachable and so needs no rewrite. +LOCAL_RENDER_ENDPOINT_HOSTS = LOCAL_RENDER_HOSTS | {DOCKER_HOST_ALIAS} +# Hosts upstream Content Agents readiness accepts as a local renderer. Mirrors +# `_LOCAL_RENDER_HOSTS` in the upstream collection's +# `apps/{material,physics}_agent_service/service/config.py`, read at +# `nvidia-omniverse/content-agents` `v0.5.2` (`36dbf3f`). Upstream matches the +# hostname exactly, so a host-local renderer named any other way -- notably the +# Docker host alias -- fails the upstream render gate. +# +# The two services differ: physics also accepts `physics-ovrtx-rendering-api`. +# Take the union, because this set is only used to decide whether upstream would +# already accept an endpoint. Over-listing means the skill skips a compensation +# it did not need; under-listing means it writes a provider credential that was +# never required. Texture has no render gate at all, so it is not mirrored here. +UPSTREAM_LOCAL_RENDER_HOSTS = { + "localhost", + "127.0.0.1", + "0.0.0.0", + "::1", + "ovrtx-rendering-api", + "physics-ovrtx-rendering-api", } +CONTAINER_RENDER_ENV_KEYS = ("RENDER_ENDPOINT", "OVRTX_RENDER_ENDPOINT", "CONTENT_AGENTS_RENDER_BASE_URL") +# usd-convert-cad 0.2 conversion runs from the self-contained pip wheel (see +# `_check_usd_convert_cad`). This checkout is cloned only as the capability and +# agent-guidance source: `usd-convert-cad`'s SKILL.md holds the authoritative +# Supported Formats table and conversion workflow guidance. It is not used as a +# conversion runtime. +UPSTREAMS = git_upstreams() @dataclass @@ -239,19 +343,19 @@ def _checkout_name_from_repo_url(repo_url: str) -> str: def _default_home() -> Path: - return Path(os.environ.get("PHYSICAL_AI_SKILL_HUB_HOME", "~/.physical-ai-skill-hub")).expanduser() + return Path(os.environ.get("OMNIVERSE_CAD_TO_SIMREADY_HOME", "~/.omniverse-cad-to-simready")).expanduser() def _default_state_root(home: Path) -> Path: - return Path(os.environ.get("PHYSICAL_AI_SKILL_HUB_STATE", home / "state")).expanduser() + return Path(os.environ.get("OMNIVERSE_CAD_TO_SIMREADY_STATE", home / "state")).expanduser() def _default_upstream_root(home: Path) -> Path: - return Path(os.environ.get("PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT", home / "upstreams")).expanduser() + return Path(os.environ.get("OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT", home / "upstreams")).expanduser() def _default_venv_root(home: Path) -> Path: - return Path(os.environ.get("PHYSICAL_AI_SKILL_HUB_VENV_ROOT", home / "venvs")).expanduser() + return Path(os.environ.get("OMNIVERSE_CAD_TO_SIMREADY_VENV_ROOT", home / "venvs")).expanduser() def _is_secret_name(name: str) -> bool: @@ -308,30 +412,599 @@ def _run( ) +def _package_versions_step( + python: Path, + package_names: tuple[str, ...], + *, + name: str, + import_check: str = "", +) -> tuple[Step, dict[str, str | None]]: + code = version_probe_code(package_names) + if import_check: + code = f"{import_check}; {code}" + step = _run([str(python), "-c", code], timeout=120) + step.name = name + if step.status == "blocked": + step.message = f"could not verify pinned Python runtime: {step.stderr_tail or step.message}" + return step, {} + try: + installed = json.loads(step.stdout_tail.strip().splitlines()[-1]) + except (IndexError, json.JSONDecodeError) as exc: + step.status = "blocked" + step.message = f"Python version probe did not return JSON: {exc}" + return step, {} + mismatches = version_mismatches(expected_versions(package_names), installed) + if mismatches: + step.status = "blocked" + step.message = "; ".join(mismatches) + else: + step.message = "pinned Python package versions match the upstream version manifest" + return step, installed + + def _container_reachable_render_endpoint(value: str) -> str: parsed = urlparse(value) if parsed.scheme not in {"http", "https"} or parsed.hostname not in LOCAL_RENDER_HOSTS: return value - host = "host.docker.internal" + host = DOCKER_HOST_ALIAS if parsed.port: host = f"{host}:{parsed.port}" return urlunparse(parsed._replace(netloc=host)) -def _content_agents_deploy_env() -> dict[str, str]: +def _endpoint_host(value: str | None) -> str: + if not value: + return "" + return (urlparse(value).hostname or "").strip("[]").lower() + + +def _is_local_render_endpoint(value: str | None) -> bool: + """Report whether a render endpoint is served by this machine. + + ``host.docker.internal`` is the Docker host alias the upstream Content + Agents collection uses to reach a host-local OVRTX renderer, so it is local + even though it is not a loopback name. Keep this predicate in agreement with + ``references/ovrtx-render-service/scripts/run.py`` and + ``references/content-agents/scripts/content_agent_client.py``. + """ + host = _endpoint_host(value) + if not host: + return False + if host in LOCAL_RENDER_ENDPOINT_HOSTS or host.endswith(".localhost"): + return True + try: + address = ipaddress.ip_address(host) + except ValueError: + return False + return address.is_loopback or address.is_unspecified + + +def _upstream_render_gate_rejects_local_endpoint(value: str | None) -> bool: + """Report whether upstream demands a render key for a host-local renderer. + + Upstream readiness matches an exact hostname allowlist, so a renderer that + is on this machine but reached through another name -- notably the + ``host.docker.internal`` Docker host alias -- fails the upstream render gate + and drives ``api_keys_configured: false`` even while rendering works over + HTTP. That is the known upstream gap behind NVBug 6534667, not a missing + workflow credential. + """ + if not _is_local_render_endpoint(value): + return False + return _endpoint_host(value) not in UPSTREAM_LOCAL_RENDER_HOSTS + + +def _content_agents_deploy_env( + model_options: dict[str, dict[str, str]] | None = None, + *, + render_endpoint: str | None = None, +) -> dict[str, str]: """Translate host-local renderer URLs for agent containers during deploy.""" env = os.environ.copy() + env.setdefault("BUILDKIT_PROGRESS", "plain") + env.setdefault("COMPOSE_BAKE", "false") for key in CONTAINER_RENDER_ENV_KEYS: if env.get(key): env[key] = _container_reachable_render_endpoint(env[key]) + if render_endpoint: + env["RENDER_ENDPOINT"] = _container_reachable_render_endpoint(render_endpoint) if not env.get("RENDER_ENDPOINT"): for key in ("OVRTX_RENDER_ENDPOINT", "CONTENT_AGENTS_RENDER_BASE_URL"): if env.get(key): env["RENDER_ENDPOINT"] = _container_reachable_render_endpoint(env[key]) break + if model_options: + vlm = model_options.get("vlm", {}) + llm = model_options.get("llm", {}) + if vlm.get("backend"): + env["CONTENT_AGENTS_VLM_BACKEND"] = vlm["backend"] + env["MA_VLM_BACKEND"] = vlm["backend"] + env["PA_VLM_BACKEND"] = vlm["backend"] + vlm_api_key_env = _content_agents_api_key_env(vlm) + if vlm_api_key_env: + env["MA_VLM_API_KEY_ENV"] = vlm_api_key_env + env["PA_VLM_API_KEY_ENV"] = vlm_api_key_env + if vlm.get("model"): + env["CONTENT_AGENTS_VLM_MODEL"] = vlm["model"] + env["MA_VLM_MODEL"] = vlm["model"] + env["PA_VLM_MODEL"] = vlm["model"] + if vlm.get("endpoint"): + env["CONTENT_AGENTS_VLM_ENDPOINT"] = vlm["endpoint"] + if vlm.get("backend", "").lower() == "openai": + env["MA_VLM_BASE_URL"] = vlm["endpoint"] + env["PA_VLM_BASE_URL"] = vlm["endpoint"] + env.pop("MA_VLM_NIM_BASE_URL", None) + env.pop("PA_VLM_NIM_BASE_URL", None) + elif vlm.get("backend", "").lower() == "nim": + env["MA_VLM_NIM_BASE_URL"] = vlm["endpoint"] + env["PA_VLM_NIM_BASE_URL"] = vlm["endpoint"] + if _content_agents_needs_default_temperature(vlm): + env["MA_VLM_TEMPERATURE"] = "1.0" + env["PA_VLM_TEMPERATURE"] = "1.0" + if llm.get("backend"): + env["CONTENT_AGENTS_LLM_BACKEND"] = llm["backend"] + env["MA_LLM_BACKEND"] = llm["backend"] + env["TA_LLM_BACKEND"] = llm["backend"] + llm_api_key_env = _content_agents_api_key_env(llm) + if llm_api_key_env: + # MA_ and TA_ deliberately differ here. The Material Agent service + # re-normalizes its pointer through format_env_reference() before + # writing its durable config (upstream + # material_agent_service/service/routers/pipeline_router.py), so a + # bare name is safe on that side and is left exactly as-is. + # + # The Texture Agent service has no such normalization: it copies + # TA_LLM_API_KEY_ENV verbatim into auto_prompt.llm.api_key_env of + # its generated pipeline config (upstream + # texture_agent_service/service/routers/pipeline_router.py, and + # texture_agent/config/unified_config.py for the local pipeline). + # ensure_no_inline_secrets() then rejects the bare name as an + # inline secret, surfacing as + # HTTPException(400, "Pipeline configuration is invalid") -- the + # second half of GH #260 / NVBug 6539618, reported at + # auto_prompt.llm.api_key_env. This is the same Material-normalizes + # / Texture-does-not asymmetry that affects image_gen below. + env["MA_LLM_API_KEY_ENV"] = llm_api_key_env + env["TA_LLM_API_KEY_ENV"] = _format_env_reference(llm_api_key_env) or llm_api_key_env + if llm.get("model"): + env["CONTENT_AGENTS_LLM_MODEL"] = llm["model"] + env["MA_LLM_MODEL"] = llm["model"] + env["TA_LLM_MODEL"] = llm["model"] + if llm.get("endpoint"): + env["CONTENT_AGENTS_LLM_ENDPOINT"] = llm["endpoint"] + if llm.get("backend", "").lower() == "openai": + env["MA_LLM_BASE_URL"] = llm["endpoint"] + env["TA_LLM_BASE_URL"] = llm["endpoint"] + env.pop("MA_LLM_NIM_BASE_URL", None) + elif llm.get("backend", "").lower() == "nim": + env["MA_LLM_NIM_BASE_URL"] = llm["endpoint"] + env["TA_LLM_BASE_URL"] = llm["endpoint"] + if _content_agents_needs_default_temperature(llm): + env["MA_LLM_TEMPERATURE"] = "1.0" + env["TA_LLM_TEMPERATURE"] = "1.0" + image_gen = model_options.get("image_gen", {}) + if image_gen.get("backend"): + env["CONTENT_AGENTS_IMAGE_GEN_BACKEND"] = image_gen["backend"] + env["MA_IMAGE_GEN_BACKEND"] = image_gen["backend"] + env["TA_IMAGE_GEN_BACKEND"] = image_gen["backend"] + # image_gen must use the durable ``${NAME}`` reference form. Unlike the + # VLM/LLM pointers -- which the Material Agent service re-normalizes via + # format_env_reference() before it writes its durable config -- the + # Texture Agent service copies TA_IMAGE_GEN_API_KEY_ENV verbatim into + # the generated pipeline config (upstream + # texture_agent_service/service/routers/pipeline_router.py). That config + # is then screened by ensure_no_inline_secrets(), which rejects a bare + # name in an ``*_api_key_env`` field and surfaces as + # HTTPException(400, "Pipeline configuration is invalid") -- the failure + # reported in GH #260 / NVBug 6539618. + # + # This value is delivered to the container through the *process* + # environment of the deploy/compose subprocess, where Compose treats it + # as an interpolation source and passes it through literally. It must + # not be written into the ``--env-file`` instead: Compose interpolates + # unquoted ``${...}`` inside env-file values, which would expand the + # reference to the raw secret. See _apply_collection_dependency. + image_gen_api_key_env = _format_env_reference( + _content_agents_api_key_env(image_gen, name="image_gen") + ) + if image_gen_api_key_env: + env["MA_IMAGE_GEN_API_KEY_ENV"] = image_gen_api_key_env + env["TA_IMAGE_GEN_API_KEY_ENV"] = image_gen_api_key_env + if image_gen.get("model"): + env["CONTENT_AGENTS_IMAGE_GEN_MODEL"] = image_gen["model"] + env["MA_IMAGE_GEN_MODEL"] = image_gen["model"] + env["TA_IMAGE_GEN_MODEL"] = image_gen["model"] + if image_gen.get("endpoint"): + env["CONTENT_AGENTS_IMAGE_GEN_ENDPOINT"] = image_gen["endpoint"] + env["MA_IMAGE_GEN_BASE_URL"] = image_gen["endpoint"] + env["TA_IMAGE_GEN_BASE_URL"] = image_gen["endpoint"] return env +def _content_agents_provider_credentials_available( + model_options: dict[str, dict[str, str]] | None = None, +) -> bool: + names = set(CONTENT_AGENTS_PROVIDER_SECRET_ENV_NAMES) + if model_options: + for group in model_options.values(): + api_key_env = _parse_env_reference(group.get("api_key_env", "")) + if api_key_env: + names.add(api_key_env) + return any(os.environ.get(name) for name in names) + + +def _env_or_arg(args: argparse.Namespace, attr: str, *env_names: str) -> str: + value = getattr(args, attr, None) + if value: + return str(value).strip() + for name in env_names: + value = os.environ.get(name) + if value: + return value.strip() + return "" + + +def _infer_content_agents_backend(model_name: str, endpoint_value: str) -> str: + normalized = model_name.lower() + if endpoint_value: + return "openai" + if normalized.startswith("claude-") or normalized.startswith("claude."): + return "anthropic" + if normalized.startswith("gemini-") or normalized.startswith("models/gemini"): + return "gemini" + if normalized.startswith(("gpt-", "o1", "o3", "o4")): + return "openai" + return "" + + +def _default_content_agents_api_key_env(backend: str, *, preferred_names: tuple[str, ...] = ()) -> str: + for name in preferred_names: + if os.environ.get(name): + return name + names = CONTENT_AGENTS_BACKEND_API_KEY_ENV_NAMES.get(backend.lower(), ()) + for name in names: + if os.environ.get(name): + return name + if preferred_names: + return preferred_names[0] + return names[0] if names else "" + + +# Mirrors upstream world_understanding/utils/credentials.py (_ENV_NAME_RE / +# _DURABLE_ENV_REFERENCE_RE). Upstream's durable boundaries -- the generated +# pipeline config written by each agent service -- accept only the unambiguous +# ``${NAME}`` reference form for ``*_api_key_env`` fields. Runtime consumers +# call parse_env_reference(allow_legacy_bare=True) and so accept both forms, +# which makes ``${NAME}`` strictly the more compatible thing to emit. +_ENV_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") +_DURABLE_ENV_REFERENCE_RE = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$") + + +def _parse_env_reference(value: str) -> str: + """Return the bare variable name carried by ``NAME`` or ``${NAME}``. + + Mirrors upstream ``parse_env_reference(..., allow_legacy_bare=True)``. + Returns "" for anything that is not a usable reference, so a + credential-bearing value is never echoed back to the caller. + """ + stripped = (value or "").strip() + match = _DURABLE_ENV_REFERENCE_RE.match(stripped) + if match: + return match.group(1) + if _ENV_NAME_RE.match(stripped): + return stripped + return "" + + +def _format_env_reference(value: str) -> str: + """Return the canonical durable ``${NAME}`` form, or "" when unusable. + + Mirrors upstream ``format_env_reference``. This only ever renders a + variable *name*; it never reads the variable's value. + """ + name = _parse_env_reference(value) + return f"${{{name}}}" if name else "" + + +def _read_credential_env(name: str) -> str: + """Read a caller-named credential env var, bounded to a well-formed name. + + api_key_env / api_key_env_name style values come from CLI flags or an env + file rather than a fixed allowlist, so this guards against forwarding an + unintended environment variable (empty string, stray whitespace, or a + malformed name) into a generated collection config or .env file. + """ + if not CREDENTIAL_ENV_NAME_PATTERN.match(name): + return "" + return os.environ.get(name, "").strip() + +def _content_agents_api_key_env(options: dict[str, str], *, name: str = "") -> str: + explicit = options.get("api_key_env", "").strip() + if explicit: + return explicit + backend = options.get("backend", "").lower() + if not backend or backend == "nim": + return "" + preferred_names = CONTENT_AGENTS_DEPENDENCY_API_KEY_ENV_NAMES.get(name, ()) + return _default_content_agents_api_key_env(backend, preferred_names=preferred_names) + + +def _content_agents_needs_default_temperature(options: dict[str, str]) -> bool: + return ( + options.get("backend", "").lower() == "openai" + and options.get("model", "").lower().startswith("gpt-") + ) + + +def _content_agents_model_options(args: argparse.Namespace) -> dict[str, dict[str, str]]: + vlm_model = _env_or_arg(args, "content_agents_vlm_model", "CONTENT_AGENTS_VLM_MODEL", "MA_VLM_MODEL", "PA_VLM_MODEL") + vlm_endpoint = _env_or_arg(args, "content_agents_vlm_endpoint", "CONTENT_AGENTS_VLM_ENDPOINT", "MA_VLM_NIM_BASE_URL", "PA_VLM_NIM_BASE_URL") + vlm_backend = _env_or_arg(args, "content_agents_vlm_backend", "CONTENT_AGENTS_VLM_BACKEND", "MA_VLM_BACKEND", "PA_VLM_BACKEND") + if not vlm_backend and (vlm_model or vlm_endpoint): + vlm_backend = _infer_content_agents_backend(vlm_model, vlm_endpoint) + vlm_api_key_env = _env_or_arg(args, "content_agents_vlm_api_key_env", "CONTENT_AGENTS_VLM_API_KEY_ENV", "MA_VLM_API_KEY_ENV", "PA_VLM_API_KEY_ENV") + + llm_model = _env_or_arg(args, "content_agents_llm_model", "CONTENT_AGENTS_LLM_MODEL", "MA_LLM_MODEL", "TA_LLM_MODEL") + llm_endpoint = _env_or_arg(args, "content_agents_llm_endpoint", "CONTENT_AGENTS_LLM_ENDPOINT", "MA_LLM_NIM_BASE_URL", "TA_LLM_BASE_URL") + llm_backend = _env_or_arg(args, "content_agents_llm_backend", "CONTENT_AGENTS_LLM_BACKEND", "MA_LLM_BACKEND", "TA_LLM_BACKEND") + if not llm_backend and (llm_model or llm_endpoint): + llm_backend = _infer_content_agents_backend(llm_model, llm_endpoint) + llm_api_key_env = _env_or_arg(args, "content_agents_llm_api_key_env", "CONTENT_AGENTS_LLM_API_KEY_ENV", "MA_LLM_API_KEY_ENV", "TA_LLM_API_KEY_ENV") + + image_gen_model = _env_or_arg(args, "content_agents_image_gen_model", "CONTENT_AGENTS_IMAGE_GEN_MODEL", "MA_IMAGE_GEN_MODEL", "TA_IMAGE_GEN_MODEL") + image_gen_endpoint = _env_or_arg(args, "content_agents_image_gen_endpoint", "CONTENT_AGENTS_IMAGE_GEN_ENDPOINT", "MA_IMAGE_GEN_BASE_URL", "TA_IMAGE_GEN_BASE_URL") + image_gen_backend = _env_or_arg(args, "content_agents_image_gen_backend", "CONTENT_AGENTS_IMAGE_GEN_BACKEND", "MA_IMAGE_GEN_BACKEND", "TA_IMAGE_GEN_BACKEND") + if not image_gen_backend and (image_gen_model or image_gen_endpoint): + image_gen_backend = _infer_content_agents_backend(image_gen_model, image_gen_endpoint) + image_gen_api_key_env = _env_or_arg(args, "content_agents_image_gen_api_key_env", "CONTENT_AGENTS_IMAGE_GEN_API_KEY_ENV", "MA_IMAGE_GEN_API_KEY_ENV", "TA_IMAGE_GEN_API_KEY_ENV") + + return { + "vlm": { + "backend": vlm_backend, + "model": vlm_model, + "endpoint": vlm_endpoint, + "api_key_env": vlm_api_key_env, + }, + "llm": { + "backend": llm_backend, + "model": llm_model, + "endpoint": llm_endpoint, + "api_key_env": llm_api_key_env, + }, + "image_gen": { + "backend": image_gen_backend, + "model": image_gen_model, + "endpoint": image_gen_endpoint, + "api_key_env": image_gen_api_key_env, + }, + } + + +def _has_content_agents_model_options(model_options: dict[str, dict[str, str]] | None) -> bool: + if not model_options: + return False + return any(value for group in model_options.values() for value in group.values()) + + +def _apply_collection_dependency( + config: dict[str, Any], + name: str, + options: dict[str, str], + *, + default_backend: str, +) -> None: + if not any(options.values()): + return + dependencies = config.setdefault("dependencies", {}) + if not isinstance(dependencies, dict): + dependencies = {} + config["dependencies"] = dependencies + dependency_config = dependencies.setdefault(name, {}) + if not isinstance(dependency_config, dict): + dependency_config = {} + dependencies[name] = dependency_config + dependency_config["enabled"] = True + dependency_config.setdefault("provider", "external") + dependency_config.setdefault("endpoint", "") + selected_backend = options.get("backend") or default_backend + dependency_config["backend"] = selected_backend + api_key_env = _content_agents_api_key_env({**options, "backend": selected_backend}, name=name) + if api_key_env: + # The collection config carries the *bare* variable name on purpose. + # Upstream deploy.py copies this value verbatim and unquoted into + # .collection.generated.env, and Docker Compose interpolates unquoted + # ``${...}`` inside an --env-file value -- so a durable ``${NAME}`` + # reference here would expand to the raw secret and hand the container + # an inline credential. The durable reference is instead exported + # through the deploy process environment (see + # _content_agents_deploy_env), which Compose passes through literally + # and which takes precedence over the --env-file value. + dependency_config.setdefault("api_key_env", _parse_env_reference(api_key_env) or api_key_env) + if options.get("model"): + dependency_config["model"] = options["model"] + if options.get("endpoint"): + if selected_backend == "openai": + dependency_config["endpoint"] = "" + dependency_config["base_url"] = options["endpoint"] + # image_gen deliberately does not forward a resolved literal key. + # Upstream's own collection.yaml ships ``api_key: not-used`` for + # image_gen, so this setdefault was already inert against real + # upstream; and the Texture Agent service copies a literal + # image_gen api_key straight into its durable pipeline config, + # where ensure_no_inline_secrets() rejects it as an inline secret. + # The ``${NAME}`` pointer wired in _content_agents_deploy_env is + # what carries the image_gen credential instead. + if api_key_env and name != "image_gen": + # api_key_env may arrive as a bare name or as ``${NAME}`` when + # an operator supplies the durable form; resolve to the bare + # variable name, then read it through the credential-name guard + # so a malformed name never reaches os.environ. + api_key_value = _read_credential_env(_parse_env_reference(api_key_env)) + if api_key_value: + dependency_config.setdefault("api_key", api_key_value) + else: + dependency_config["endpoint"] = options["endpoint"] + if _content_agents_needs_default_temperature(options): + dependency_config["temperature"] = 1.0 + + +def _content_agents_openai_endpoint_options( + model_options: dict[str, dict[str, str]] | None, +) -> list[str]: + if not model_options: + return [] + return [ + name + for name, options in model_options.items() + if options.get("backend", "").lower() == "openai" and bool(options.get("endpoint")) + ] + + +def _collection_deploy_supports_openai_base_url(deploy_py: Path) -> bool: + try: + text = deploy_py.read_text(encoding="utf-8") + except OSError: + return False + if "OPENAI_BASE_URL" in text: + return True + return ( + "set_model_endpoint_env" in text + and 'vlm_backend == "openai"' in text + and 'llm_backend == "openai"' in text + and "_BASE_URL" in text + and "backend=openai uses base_url" in text + ) + + +def _prepare_content_agents_collection_config( + world_root: Path, + *, + include_texture: bool, + model_options: dict[str, dict[str, str]] | None, + enabled_agents: set[str] | None = None, + render_endpoint: str | None = None, + config_suffix: str = "", +) -> tuple[Path | None, Step | None]: + """Write a non-secret upstream collection config when local overrides exist.""" + deploy_py = world_root / "deploy" / "collection" / "deploy.py" + if not deploy_py.is_file(): + return None, None + openai_endpoint_roles = _content_agents_openai_endpoint_options(model_options) + if openai_endpoint_roles and not _collection_deploy_supports_openai_base_url(deploy_py): + return None, Step( + name="content_agents_collection_config", + status="blocked", + message=( + "upstream Content Agents collection deploy does not support " + "OpenAI backend base_url deployment for " + + ", ".join(sorted(openai_endpoint_roles)) + ), + ) + + needs_generated_config = ( + include_texture + or enabled_agents is not None + or bool(render_endpoint) + or _has_content_agents_model_options(model_options) + ) + if not needs_generated_config: + return None, None + + base_config = world_root / "deploy" / "collection" / "collection.yaml" + if not base_config.is_file(): + return None, Step( + name="content_agents_collection_config", + status="blocked", + message="upstream Content Agents collection.yaml was not found", + ) + + try: + config = yaml.safe_load(base_config.read_text(encoding="utf-8")) or {} + except (OSError, yaml.YAMLError) as exc: + return None, Step( + name="content_agents_collection_config", + status="blocked", + message=f"upstream Content Agents collection.yaml could not be read: {exc}", + ) + if not isinstance(config, dict): + return None, Step( + name="content_agents_collection_config", + status="blocked", + message="upstream Content Agents collection.yaml must contain a YAML mapping", + ) + + agents = config.setdefault("agents", {}) + if not isinstance(agents, dict): + agents = {} + config["agents"] = agents + if enabled_agents is not None: + for name in ("material", "physics", "texture"): + entry = agents.setdefault(name, {}) + if not isinstance(entry, dict): + entry = {} + agents[name] = entry + entry["enabled"] = name in enabled_agents + elif include_texture: + texture = agents.setdefault("texture", {}) + if not isinstance(texture, dict): + texture = {} + agents["texture"] = texture + texture["enabled"] = True + + if render_endpoint: + dependencies = config.setdefault("dependencies", {}) + if not isinstance(dependencies, dict): + dependencies = {} + config["dependencies"] = dependencies + render = dependencies.setdefault("render", {}) + if not isinstance(render, dict): + render = {} + dependencies["render"] = render + render["enabled"] = True + render["provider"] = "external" + render["endpoint"] = _container_reachable_render_endpoint(render_endpoint) + + options = model_options or {} + _apply_collection_dependency(config, "vlm", options.get("vlm", {}), default_backend="nim") + _apply_collection_dependency(config, "llm", options.get("llm", {}), default_backend="nim") + _apply_collection_dependency(config, "image_gen", options.get("image_gen", {}), default_backend="openai") + + suffix = f".{config_suffix}" if config_suffix else "" + generated_config = world_root / "deploy" / "collection" / f".collection.cad-to-simready{suffix}.yaml" + try: + generated_config.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8") + except OSError as exc: + return None, Step( + name="content_agents_collection_config", + status="blocked", + message=f"upstream Content Agents collection config could not be written: {exc}", + ) + try: + generated_config.chmod(0o600) + except OSError: + pass + + configured: list[str] = [] + if enabled_agents is not None: + configured.append("agents=" + ",".join(sorted(enabled_agents))) + if include_texture: + configured.append("texture") + if render_endpoint: + configured.append("render") + if _has_content_agents_model_options({"vlm": options.get("vlm", {})}): + configured.append("vlm") + if _has_content_agents_model_options({"llm": options.get("llm", {})}): + configured.append("llm") + if _has_content_agents_model_options({"image_gen": options.get("image_gen", {})}): + configured.append("image_gen") + return generated_config, Step( + name="content_agents_collection_config", + status="ready", + message=f"wrote upstream collection override for {', '.join(configured)}", + command=[], + ) + + def _path_entries(*entries: Path | None) -> list[str]: seen: set[str] = set() values: list[str] = [] @@ -511,7 +1184,9 @@ def _git_dirty(path: Path) -> bool: def _ensure_upstream(name: str, upstream_root: Path, *, check_only: bool, no_update: bool) -> tuple[dict[str, Any], list[Step]]: spec = UPSTREAMS[name] path = _upstream_path(name, upstream_root) - branch = spec["branch"] + expected_ref = str(spec["ref"]) + expected_commit = str(spec["commit"]) + explicitly_overridden = bool(os.environ.get(str(spec["env"]))) steps: list[Step] = [] if not path.exists(): if check_only: @@ -520,37 +1195,69 @@ def _ensure_upstream(name: str, upstream_root: Path, *, check_only: bool, no_upd "status": "blocked", "path": str(path), "url": spec["url"], - "branch": branch, - "message": f"checkout is missing: {path}", + "ref": expected_ref, + "expected_commit": expected_commit, + "version_manifest": str(UPSTREAM_VERSION_MANIFEST_PATH), + "message": f"checkout is missing: {path}; requires {expected_ref} ({expected_commit})", }, steps, ) path.parent.mkdir(parents=True, exist_ok=True) - command = ["git", "clone", str(spec["url"]), str(path)] - if branch: - command = ["git", "clone", "--branch", str(branch), str(spec["url"]), str(path)] - steps.append(_run(command, timeout=1800)) + clone_env = os.environ.copy() + clone_env["GIT_LFS_SKIP_SMUDGE"] = "1" + command = [ + "git", + "clone", + "--filter=blob:none", + "--depth", + "1", + "--single-branch", + "--branch", + expected_ref, + str(spec["url"]), + str(path), + ] + steps.append(_run(command, env=clone_env, timeout=1800)) elif (path / ".git").exists() and not check_only and not no_update: - if _git_dirty(path): + if explicitly_overridden: + steps.append( + Step( + name=f"{name}_update", + status="skipped", + message="explicit upstream override is never mutated; verifying its pinned commit", + ) + ) + elif _git_dirty(path): steps.append(Step(name=f"{name}_update", status="skipped", message="checkout has local changes; skipped automatic update")) else: - if branch: - steps.append(_run(["git", "-C", str(path), "fetch", "origin", str(branch)], timeout=600)) - steps.append(_run(["git", "-C", str(path), "checkout", str(branch)], timeout=120)) - steps.append(_run(["git", "-C", str(path), "pull", "--ff-only"], timeout=600)) + steps.append(_run(["git", "-C", str(path), "fetch", "--depth", "1", "origin", expected_ref], timeout=600)) + if steps[-1].status != "blocked": + steps.append(_run(["git", "-C", str(path), "checkout", "--detach", expected_commit], timeout=120)) present = path.exists() - status = "present" if present else "blocked" - if any(step.status == "blocked" for step in steps): + actual_commit = _git_commit(path) + matches_pin = actual_commit == expected_commit + status = "ready" if present and matches_pin else "blocked" + if any(step.status == "blocked" for step in steps) or not (path / ".git").exists(): status = "blocked" + if not present: + message = f"checkout is missing: {path}; requires {expected_ref} ({expected_commit})" + elif actual_commit is None: + message = f"checkout at {path} is not a Git worktree; requires {expected_ref} ({expected_commit})" + elif not matches_pin: + message = f"requires {expected_ref} ({expected_commit}), found {actual_commit} at {path}" + else: + message = f"checkout matches pinned {expected_ref} ({expected_commit})" return ( { "status": status, "path": str(path), "url": spec["url"], - "branch": branch, - "commit": _git_commit(path), - "message": "checkout is present" if present else f"checkout is missing: {path}", + "ref": expected_ref, + "expected_commit": expected_commit, + "commit": actual_commit, + "version_manifest": str(UPSTREAM_VERSION_MANIFEST_PATH), + "message": message, }, steps, ) @@ -562,6 +1269,23 @@ def _runtime_entry(status: str, message: str, **extra: Any) -> dict[str, Any]: return payload +def _manifest_blockers( + upstreams: dict[str, dict[str, Any]], + runtimes: dict[str, dict[str, Any]], + services: dict[str, dict[str, Any]], +) -> list[str]: + blockers: list[str] = [] + for category, entries in ( + ("upstream", upstreams), + ("runtime", runtimes), + ("service", services), + ): + for name, entry in sorted(entries.items()): + if entry.get("status") == "blocked": + blockers.append(f"{category} {name}: {entry.get('message')}") + return blockers + + def _nearest_existing_parent(path: Path) -> Path: current = path while not current.exists() and current.parent != current: @@ -690,50 +1414,101 @@ def _check_git_lfs(*, install_lfs: bool, check_only: bool) -> tuple[dict[str, An return _runtime_entry(status, "Git LFS is available", executable=git_lfs), steps -def _check_usd_convert_cad(root: Path, *, project_root: Path | None, check_only: bool) -> tuple[dict[str, Any], list[Step]]: +def _resolve_usd_convert_cad_python(project_root: Path | None) -> Path: + """Resolve the interpreter expected to hold the usd-convert-cad wheel. + + The wheel bundles its own ``pxr`` runtime, so it belongs in a dedicated + interpreter. Order: ``USD_CONVERT_CAD_PYTHON`` env -> the project ``.venv`` + -> the current interpreter. + """ + env_python = os.environ.get(USD_CONVERT_CAD_PYTHON_ENV) + if env_python: + # Do not call Path.resolve(): venv/bin/python is commonly a symlink to + # the base interpreter, and dereferencing it loses the venv package + # context when that target is executed directly. + return Path(os.path.abspath(os.fspath(Path(env_python).expanduser()))) + return _project_venv_python(project_root) or Path(sys.executable) + + +def _usd_convert_cad_skill_md(skill_source: dict[str, Any] | None) -> str | None: + """Locate the SKILL.md inside a cloned usd-convert-cad checkout, if present.""" + if not skill_source: + return None + root = skill_source.get("path") + if not root: + return None + checkout = Path(str(root)) + candidates = [ + checkout / "skills" / "omniverse-cad-to-usd" / "SKILL.md", + checkout / ".agents" / "skills" / "usd-convert-cad" / "SKILL.md", + ] + for candidate in candidates: + if candidate.is_file(): + return str(candidate) + if checkout.exists(): + matches = sorted(checkout.glob("**/SKILL.md")) + if matches: + return str(matches[0]) + return None + + +def _check_usd_convert_cad( + *, + project_root: Path | None, + check_only: bool, + skill_source: dict[str, Any] | None = None, +) -> tuple[dict[str, Any], list[Step]]: steps: list[Step] = [] - install_py = root / "install.py" - validate_py = root / "validate.py" - convert_py = root / "convert.py" - if not root.exists(): - return _runtime_entry("blocked", "usd-convert-cad checkout is missing", root=str(root)), steps - if not convert_py.is_file() or not validate_py.is_file(): - return _runtime_entry("blocked", "usd-convert-cad convert.py or validate.py is missing", root=str(root)), steps - env = os.environ.copy() - env.setdefault("OMNI_KIT_ACCEPT_EULA", "yes") - if not check_only: - python = str(_project_venv_python(project_root) or Path(sys.executable)) - command_env = env.copy() - extra_path = _env_with_extra_path(_project_venv_bin(project_root)).get("PATH") - if extra_path: - command_env["PATH"] = extra_path - validate = _run([python, str(validate_py)], cwd=root, env=command_env, timeout=900) - steps.append(validate) - if validate.status == "blocked" and install_py.is_file(): - install = _run([python, str(install_py)], cwd=root, env=command_env, timeout=3600) - steps.append(install) - validate = _run([python, str(validate_py)], cwd=root, env=command_env, timeout=900) - steps.append(validate) - status = "ready" if check_only or (steps and steps[-1].status == "ready") else "blocked" - message = "usd-convert-cad is installed and validated" if status == "ready" else "usd-convert-cad install or validation failed" - if check_only: - message = "usd-convert-cad files are present; runtime validation was not run" - runtime = _runtime_entry(status, message, root=str(root), executable=str(convert_py)) - if status == "blocked": - final_validate = next( - ( - step - for step in reversed(steps) - if len(step.command) > 1 and Path(step.command[1]).name == "validate.py" - ), - None, + python = _resolve_usd_convert_cad_python(project_root) + skill_md = _usd_convert_cad_skill_md(skill_source) + if not python.exists(): + runtime = _runtime_entry( + "blocked", + f"usd-convert-cad interpreter was not found: {python}. {USD_CONVERT_CAD_INSTALL_HINT}", + executable=str(python), + skill_md=skill_md, + ) + return runtime, steps + + # A self-contained wheel is "ready" when it imports in the resolved + # interpreter. This is a fast check, so run it even in --check-only mode; + # preflight never pip-installs the wheel implicitly (see the install hint). + import_step = _run([str(python), "-c", USD_CONVERT_CAD_IMPORT_CHECK], timeout=120) + steps.append(import_step) + installed_versions: dict[str, str | None] = {} + if import_step.status == "ready": + version_step, installed_versions = _package_versions_step( + python, + ("usd-convert-cad",), + name="usd_convert_cad_version_gate", ) - if final_validate is not None: - output = "\n".join(part for part in (final_validate.stdout_tail, final_validate.stderr_tail) if part) - diagnostic = summarize_usd_convert_cad_validation_failure(output, final_validate.returncode) - if diagnostic: - runtime["message"] = f"{message}: {diagnostic['summary']} {diagnostic['recovery_hint']}" - runtime["diagnostics"] = [diagnostic] + steps.append(version_step) + status = "ready" if steps[-1].status == "ready" else "blocked" + if status == "ready": + version = installed_versions["usd-convert-cad"] + message = f"usd-convert-cad wheel matches pinned version {version}" + if skill_md: + message = f"{message}; capability/guidance SKILL.md: {skill_md}" + elif import_step.status == "ready": + message = steps[-1].message + else: + detail = "\n".join(part for part in (import_step.stdout_tail, import_step.stderr_tail) if part) + message = ( + f"usd-convert-cad wheel is not importable with {python}. {USD_CONVERT_CAD_INSTALL_HINT}" + ) + if detail: + message = f"{message} Output: {tail_text(detail)}" + runtime = _runtime_entry( + status, + message, + executable=str(python), + skill_md=skill_md, + version=installed_versions.get("usd-convert-cad"), + required_version=USD_CONVERT_CAD_VERSION, + version_manifest=str(UPSTREAM_VERSION_MANIFEST_PATH), + ) + if status == "blocked": + runtime["install_hint"] = USD_CONVERT_CAD_INSTALL_HINT return runtime, steps @@ -760,7 +1535,13 @@ def _simready_executable(venv_dir: Path) -> Path: def _simready_runtime_import_check(venv_dir: Path) -> Step: python = _venv_bin_dir(venv_dir) / ("python.exe" if os.name == "nt" else "python") - return _run([str(python), "-c", "import numpy"], timeout=60) + step, _ = _package_versions_step( + python, + SIMREADY_RUNTIME_PACKAGE_NAMES, + name="simready_validate_runtime_check", + import_check="import numpy; import omni.asset_validator; from pxr import Usd", + ) + return step def _foundation_requirements_path(root: Path) -> Path: @@ -771,47 +1552,6 @@ def _foundation_requirements_path(root: Path) -> Path: return next((path for path in candidates if path.is_file()), candidates[0]) -def _dedupe_requirements(requirements: list[str]) -> list[str]: - seen: set[str] = set() - deduped: list[str] = [] - for requirement in requirements: - key = requirement.strip().lower() - if not key or key in seen: - continue - seen.add(key) - deduped.append(requirement) - return deduped - - -def _foundation_simready_requirements(requirements_path: Path) -> tuple[list[str], list[str]]: - simready_requirements: list[str] = [] - other_requirements: list[str] = [] - for raw_line in requirements_path.read_text(encoding="utf-8").splitlines(): - line = raw_line.split("#", 1)[0].strip() - if not line: - continue - if line.lower().startswith("simready-validate"): - simready_requirements.append(line) - elif not line.lower().startswith("usd-core"): - other_requirements.append(line) - return simready_requirements or [DEFAULT_SIMREADY_VALIDATE_REQUIREMENT], other_requirements - - -def _is_aarch64() -> bool: - return platform.machine().lower() in {"aarch64", "arm64"} - - -def _simready_install_detail(step: Step) -> str: - return "\n".join(part for part in (step.stdout_tail, step.stderr_tail, step.message) if part) - - -def _should_try_simready_usd_exchange_fallback(step: Step) -> bool: - if step.status != "blocked" or not _is_aarch64(): - return False - lowered = _simready_install_detail(step).lower() - return "usd-core" in lowered or "no matching distribution" in lowered or "resolutionimpossible" in lowered - - def _simready_pip_install_command(python: Path, requirements: list[str], *, uv: str | None) -> list[str]: if uv: return [uv, "pip", "install", "--python", str(python), *requirements] @@ -822,22 +1562,15 @@ def _simready_pip_install_step(python: Path, requirements: list[str], *, uv: str return _run(_simready_pip_install_command(python, requirements, uv=uv), timeout=1800) -def _install_simready_with_usd_exchange_sdk_runtime(python: Path, requirements_path: Path, *, uv: str | None) -> list[Step]: - simready_requirements, other_requirements = _foundation_simready_requirements(requirements_path) - runtime_requirements = _dedupe_requirements( - [*USD_EXCHANGE_SDK_FALLBACK_REQUIREMENTS, *other_requirements, *SIMREADY_RUNTIME_EXTRA_REQUIREMENTS] - ) - steps = [_simready_pip_install_step(python, runtime_requirements, uv=uv)] +def _install_simready_locked_runtime(python: Path, *, uv: str | None) -> list[Step]: + steps = [_simready_pip_install_step(python, list(SIMREADY_RUNTIME_REQUIREMENTS), uv=uv)] if steps[-1].status != "blocked": - steps.append(_simready_pip_install_step(python, ["--no-deps", *simready_requirements], uv=uv)) + steps.append(_simready_pip_install_step(python, ["--no-deps", *SIMREADY_NO_DEPS_REQUIREMENTS], uv=uv)) return steps def _check_simready(root: Path, venv_root: Path, *, project_root: Path | None, check_only: bool) -> tuple[dict[str, Any], list[Step]]: requirements = _foundation_requirements_path(root) - executable = _which("simready-validate") - if executable: - return _runtime_entry("ready", "simready-validate executable is on PATH", root=str(root), executable=executable), [] if not root.exists(): return _runtime_entry("blocked", "SimReady Foundation checkout is missing", root=str(root)), [] if not requirements.is_file(): @@ -848,6 +1581,16 @@ def _check_simready(root: Path, venv_root: Path, *, project_root: Path | None, c if venv_executable.is_file(): import_check = _simready_runtime_import_check(venv_dir) if import_check.status == "blocked": + if check_only: + return _runtime_entry( + "blocked", + f"existing simready-validate runtime does not match the pinned package set: {import_check.message}", + root=str(root), + executable=str(venv_executable), + venv=str(venv_dir), + required_versions=expected_versions(SIMREADY_RUNTIME_PACKAGE_NAMES), + version_manifest=str(UPSTREAM_VERSION_MANIFEST_PATH), + ), [import_check] steps.append( Step( name="simready_validate_runtime_check", @@ -860,13 +1603,21 @@ def _check_simready(root: Path, venv_root: Path, *, project_root: Path | None, c steps.append(import_check) return _runtime_entry( "ready", - "simready-validate executable is available from the preflight venv", + "simready-validate pinned runtime is available from the preflight venv", root=str(root), executable=str(venv_executable), venv=str(venv_dir), + required_versions=expected_versions(SIMREADY_RUNTIME_PACKAGE_NAMES), + version_manifest=str(UPSTREAM_VERSION_MANIFEST_PATH), ), steps if check_only: - return _runtime_entry("blocked", "simready-validate is installable from Foundation requirements but was not installed in check-only mode", root=str(root)), [] + return _runtime_entry( + "blocked", + "the pinned simready-validate runtime was not installed in check-only mode", + root=str(root), + required_versions=expected_versions(SIMREADY_RUNTIME_PACKAGE_NAMES), + version_manifest=str(UPSTREAM_VERSION_MANIFEST_PATH), + ), [] uv = _which("uv") if uv: venv_command = [uv, "venv", "--python", "3.12"] @@ -877,9 +1628,7 @@ def _check_simready(root: Path, venv_root: Path, *, project_root: Path | None, c steps = [_run([sys.executable, "-m", "venv", str(venv_dir)], env=_env_with_extra_path(_project_venv_bin(project_root)), timeout=300)] python = _venv_bin_dir(venv_dir) / ("python.exe" if os.name == "nt" else "python") if steps[-1].status != "blocked": - steps.append(_simready_pip_install_step(python, ["-r", str(requirements), *SIMREADY_RUNTIME_EXTRA_REQUIREMENTS], uv=uv)) - if _should_try_simready_usd_exchange_fallback(steps[-1]): - steps.extend(_install_simready_with_usd_exchange_sdk_runtime(python, requirements, uv=uv)) + steps.extend(_install_simready_locked_runtime(python, uv=uv)) if steps[-1].status != "blocked": steps.append(_simready_runtime_import_check(venv_dir)) status = "ready" if venv_executable.is_file() and steps[-1].status != "blocked" else "blocked" @@ -889,6 +1638,8 @@ def _check_simready(root: Path, venv_root: Path, *, project_root: Path | None, c root=str(root), executable=str(venv_executable) if venv_executable.exists() else "", venv=str(venv_dir), + required_versions=expected_versions(SIMREADY_RUNTIME_PACKAGE_NAMES), + version_manifest=str(UPSTREAM_VERSION_MANIFEST_PATH), ), steps @@ -1034,7 +1785,31 @@ def _probe_ovrtx_render_smoke(base_url: str) -> dict[str, Any]: } -def _probe_service(service: str, base_url: str, timeout: int = 8) -> dict[str, Any]: +def _missing_api_keys_message(service: str, render_endpoint: str | None) -> str: + """Explain a Content Agents ``api_keys_configured: false`` health payload. + + A remote render endpoint is the actionable cause worth naming: upstream + Material/Physics readiness demands a render usage key for any renderer that + is not on this machine, so say which key is missing and why. + """ + message = f"{service} health endpoint responded but API keys are not configured" + if render_endpoint and not _is_local_render_endpoint(render_endpoint): + host = _endpoint_host(render_endpoint) or render_endpoint + message += ( + f"; the render endpoint host {host} is remote, so the {service} agent requires" + " a render usage key in NGC_API_KEY or NVCF_API_KEY" + ) + return message + + +def _probe_service( + service: str, + base_url: str, + timeout: int = 8, + *, + render_endpoint: str | None = None, + provided: bool = False, +) -> dict[str, Any]: if _is_remote_or_invocation_endpoint(base_url): return { "status": "ready", @@ -1083,11 +1858,30 @@ def _probe_service(service: str, base_url: str, timeout: int = 8) -> dict[str, A "render_smoke": smoke, } if service in {"material", "physics", "texture"} and health_payload.get("api_keys_configured") is False: + if provided and _upstream_render_gate_rejects_local_endpoint(render_endpoint): + # Reuse path: the user owns this already-running + # agent, so preflight cannot mirror a render key + # into it the way managed deployment does. The + # configured renderer is on this machine under a + # name upstream's allowlist misses, which is enough + # to explain the flag, so do not block a service + # that renders fine over HTTP. + return { + "status": "ready", + "base_url": base_url.rstrip("/"), + "health_url": url, + "message": ( + f"{service} provided endpoint accepted; its `api_keys_configured: false` is" + f" explained by the host-local render endpoint {_endpoint_host(render_endpoint)}," + " which upstream readiness does not recognize as local" + ), + "health_response": body, + } return { "status": "blocked", "base_url": base_url.rstrip("/"), "health_url": url, - "message": f"{service} health endpoint responded but API keys are not configured", + "message": _missing_api_keys_message(service, render_endpoint), "health_response": body, } return { @@ -1126,15 +1920,125 @@ def _deploy_ovrtx(world_root: Path) -> Step: command=["docker", "compose", "-f", str(compose_file), "up", "-d", "--build"], ) env = os.environ.copy() + env.setdefault("BUILDKIT_PROGRESS", "plain") + env.setdefault("COMPOSE_BAKE", "false") env.setdefault("OVRTX_RENDER_MODE", "pt") + command = ["docker", "compose", "-f", str(compose_file), "up", "-d"] + if not _docker_image_exists("ovrtx-rendering-api:local"): + command.append("--build") return _run( - ["docker", "compose", "-f", str(compose_file), "up", "-d", "--build"], + command, cwd=world_root, env=env, timeout=3600, ) +def _docker_image_exists(image: str) -> bool: + if _which("docker") is None: + return False + try: + completed = subprocess.run( + ["docker", "image", "inspect", image], + capture_output=True, + text=True, + timeout=30, + check=False, + ) + except (OSError, subprocess.TimeoutExpired): + return False + return completed.returncode == 0 + + +def _collection_agent_service_name(service: str) -> str: + return f"{service}-agent-service" + + +def _collection_agent_image_name(service: str) -> str: + return f"content-agents-{service}-agent-service" + + +def _deploy_content_agent_service( + world_root: Path, + service: str, + *, + include_texture: bool, + model_options: dict[str, dict[str, str]] | None, + render_endpoint: str | None, +) -> list[Step]: + deploy_py = world_root / "deploy" / "collection" / "deploy.py" + if not deploy_py.is_file(): + return [ + Step( + name=f"{service}_agent_deploy", + status="blocked", + message="upstream Content Agents collection deploy.py was not found", + command=[sys.executable, str(deploy_py), "up"], + ) + ] + + config_path, config_step = _prepare_content_agents_collection_config( + world_root, + include_texture=include_texture, + model_options=model_options, + enabled_agents={service}, + render_endpoint=render_endpoint, + config_suffix=service, + ) + steps: list[Step] = [] + if config_step is not None: + steps.append(config_step) + if config_step.status == "blocked": + return steps + if config_path is None: + return [ + Step( + name=f"{service}_agent_deploy", + status="blocked", + message="failed to prepare deterministic Content Agents collection config", + command=[sys.executable, str(deploy_py), "up"], + ) + ] + + env = _content_agents_deploy_env(model_options, render_endpoint=render_endpoint) + image_name = _collection_agent_image_name(service) + service_name = _collection_agent_service_name(service) + if _docker_image_exists(image_name): + render_env_command = [sys.executable, str(deploy_py), "-c", str(config_path), "render-env"] + steps.append(_run(render_env_command, cwd=world_root, env=env, timeout=300)) + steps[-1].name = f"{service}_agent_render_env" + if steps[-1].status == "blocked": + return steps + + generated_env = world_root / "deploy" / "collection" / ".collection.generated.env" + compose_file = world_root / "deploy" / "collection" / "docker-compose.agents.yml" + command = [ + "docker", + "compose", + "--env-file", + str(generated_env), + "-p", + "content-agents", + "-f", + str(compose_file), + "up", + "-d", + service_name, + ] + else: + command = [sys.executable, str(deploy_py), "-c", str(config_path), "up"] + steps.append( + _run( + command, + cwd=world_root, + env=env, + timeout=3600, + ) + ) + steps[-1].name = f"{service}_agent_deploy" + return steps + + def _check_content_agents_deployment_host() -> dict[str, Any]: checks: list[dict[str, Any]] = [] blockers: list[str] = [] @@ -1185,20 +2089,67 @@ def _should_check_content_agents_deployment_host(service_reports: dict[str, Any] return any(report.get("status") == "blocked" and not _service_url_was_provided(service) for service, report in service_reports.items()) +def _content_agent_services(include_texture: bool) -> tuple[str, ...]: + services = ["material", "physics"] + if include_texture: + services.append("texture") + return tuple(services) + + +def _agent_needs_render_endpoint(service: str) -> bool: + return service in {"material", "physics"} + + def _check_content_agents( world_root: Path, *, include_texture: bool, check_only: bool, skip_deploy: bool, + model_options: dict[str, dict[str, str]] | None = None, ) -> tuple[dict[str, Any], dict[str, Any], list[Step]]: - services = ("ovrtx", "material", "physics", "texture") if include_texture else ("ovrtx", "material", "physics") - service_reports = {service: _probe_service(service, _service_env_url(service)) for service in services} - if all(report["status"] == "ready" for report in service_reports.values()): + agent_services = _content_agent_services(include_texture) + agents_to_deploy = [service for service in agent_services if not _service_url_was_provided(service)] + needs_render = any(_agent_needs_render_endpoint(service) for service in agents_to_deploy) + services = list(agent_services) + if needs_render or _service_url_was_provided("ovrtx"): + services.insert(0, "ovrtx") + configured_render_endpoint = _service_env_url("ovrtx") + service_reports = { + service: _probe_service( + service, + _service_env_url(service), + render_endpoint=None if service == "ovrtx" else configured_render_endpoint, + provided=_service_url_was_provided(service), + ) + for service in services + } + + explicit_blockers = [ + service + for service, report in service_reports.items() + if _service_url_was_provided(service) and report.get("status") == "blocked" + ] + if explicit_blockers: + message = "explicit Content Agents endpoint(s) are not healthy: " + ", ".join(explicit_blockers) + if explicit_blockers == ["ovrtx"]: + message = "explicit renderer endpoint is not healthy" + return ( + _runtime_entry( + "blocked", + message, + root=str(world_root), + ), + service_reports, + [], + ) + + if not agents_to_deploy and not needs_render and all(report["status"] == "ready" for report in service_reports.values()): return _runtime_entry("ready", "Content Agents services are already healthy", root=str(world_root)), service_reports, [] steps: list[Step] = [] - if _should_check_content_agents_deployment_host(service_reports, will_deploy=not (check_only or skip_deploy)): + will_deploy = not (check_only or skip_deploy) and bool(agents_to_deploy or needs_render) + if _should_check_content_agents_deployment_host(service_reports, will_deploy=will_deploy): deployment_host = _check_content_agents_deployment_host() else: deployment_host = _runtime_entry( @@ -1232,60 +2183,87 @@ def _check_content_agents( return _runtime_entry("blocked", "docker was not found on PATH", root=str(world_root), deployment_host=deployment_host), service_reports, steps if deployment_host.get("status") == "blocked": return _runtime_entry("blocked", "Content Agents local deployment host is not ready", root=str(world_root), deployment_host=deployment_host), service_reports, steps - if not os.environ.get("NVIDIA_API_KEY"): - return _runtime_entry("blocked", "NVIDIA_API_KEY is required for managed local Content Agents deployment", root=str(world_root), deployment_host=deployment_host), service_reports, steps - - steps.append(_ensure_content_agents_secret_env(world_root)) - if steps[-1].status == "blocked": - return _runtime_entry("blocked", "failed to prepare upstream Content Agents credential environment", root=str(world_root)), service_reports, steps - - targets = ["ovrtx", "material", "physics"] - if include_texture: - targets.append("texture") - - # Keep deployment ownership upstream. Prefer the collection deploy wrapper - # when available; fall back to older script-shaped deploy references. - deploy_commands: list[tuple[Path, list[str]]] = [] - for path in ( - world_root / ".agents" / "skills" / "deploy-collection" / "scripts" / "deploy_collection.sh", - world_root / ".codex" / "skills" / "deploy-collection" / "scripts" / "deploy_collection.sh", - ): - command = [str(path), "up"] if os.access(path, os.X_OK) else ["bash", str(path), "up"] - deploy_commands.append((path, command)) - for path in ( - world_root / ".agents" / "skills" / "deploy-content-agents" / "scripts" / "run.py", - world_root / ".codex" / "skills" / "deploy-content-agents" / "scripts" / "run.py", - world_root / "scripts" / "deploy_content_agents.py", - ): - deploy_commands.append((path, [sys.executable, str(path), "--targets", ",".join(targets)])) - - deploy_script, deploy_command = next(((path, command) for path, command in deploy_commands if path.is_file()), (None, [])) - if deploy_script is None: + if not _content_agents_provider_credentials_available(model_options): return ( _runtime_entry( "blocked", - "content-agents checkout is present, but no upstream Content Agents deployment script was found; run the upstream deployment skills or provide healthy endpoints", + "a Content Agents provider API key is required for managed local deployment; set one of NVIDIA_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, or GEMINI_API_KEY", root=str(world_root), + deployment_host=deployment_host, ), service_reports, steps, + ) + + if agents_to_deploy: + steps.append( + _ensure_content_agents_secret_env( + world_root, + model_options=model_options, + render_endpoint=_service_env_url("ovrtx") if needs_render else None, + ) ) - steps.append(_run(deploy_command, cwd=world_root, env=_content_agents_deploy_env(), timeout=3600)) - service_reports = {service: _probe_service(service, _service_env_url(service), timeout=30) for service in services} - if steps[-1].status != "blocked" and service_reports.get("ovrtx", {}).get("status") != "ready": - # The upstream collection helper starts agent services, while the - # renderer remains owned by the upstream standalone OVRTX deployment. - # Invoke that upstream Compose entrypoint as the second managed - # deployment step when the renderer was not already healthy. + if steps[-1].status == "blocked": + return _runtime_entry("blocked", "failed to prepare upstream Content Agents credential environment", root=str(world_root)), service_reports, steps + + render_endpoint: str | None = None + if needs_render and _service_url_was_provided("ovrtx"): + render_endpoint = service_reports["ovrtx"]["base_url"] + elif needs_render: steps.append(_deploy_ovrtx(world_root)) - service_reports = { - service: _probe_service( - service, - _service_env_url(service), - timeout=300 if service == "ovrtx" else 30, + if steps[-1].status == "blocked": + return _runtime_entry("blocked", "failed to deploy upstream OVRTX renderer", root=str(world_root)), service_reports, steps + service_reports["ovrtx"] = _probe_service("ovrtx", _service_env_url("ovrtx"), timeout=300) + if service_reports["ovrtx"].get("status") != "ready": + return ( + _runtime_entry( + "blocked", + "upstream OVRTX renderer did not become healthy before agent deployment", + root=str(world_root), + ), + service_reports, + steps, ) - for service in services - } + render_endpoint = service_reports["ovrtx"]["base_url"] + + for service in agent_services: + if _service_url_was_provided(service): + continue + service_steps = _deploy_content_agent_service( + world_root, + service, + include_texture=include_texture, + model_options=model_options, + render_endpoint=render_endpoint if _agent_needs_render_endpoint(service) else render_endpoint, + ) + steps.extend(service_steps) + if any(step.status == "blocked" for step in service_steps): + return ( + _runtime_entry( + "blocked", + f"failed to deploy upstream Content Agents {service} service", + root=str(world_root), + ), + service_reports, + steps, + ) + service_reports[service] = _probe_service( + service, + _service_env_url(service), + timeout=300, + render_endpoint=render_endpoint or configured_render_endpoint, + ) + if service_reports[service].get("status") != "ready": + return ( + _runtime_entry( + "blocked", + f"upstream Content Agents {service} service did not become healthy after deployment", + root=str(world_root), + ), + service_reports, + steps, + ) + status = "ready" if all(report["status"] == "ready" for report in service_reports.values()) else "blocked" return _runtime_entry(status, "Content Agents services are healthy" if status == "ready" else "Content Agents deployment did not produce healthy endpoints", root=str(world_root)), service_reports, steps @@ -1294,9 +2272,9 @@ def _env_payload(manifest_path: Path, manifest: dict[str, Any]) -> dict[str, str env: dict[str, str] = { "PHYSICAL_AI_PREFLIGHT_MANIFEST": str(manifest_path), "PHYSICAL_AI_REQUIRE_PREFLIGHT": "1", - "PHYSICAL_AI_SKILL_HUB_HOME": manifest["paths"]["home"], - "PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT": manifest["paths"]["upstream_root"], - "PHYSICAL_AI_SKILL_HUB_STATE": manifest["paths"]["state_root"], + "OMNIVERSE_CAD_TO_SIMREADY_HOME": manifest["paths"]["home"], + "OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT": manifest["paths"]["upstream_root"], + "OMNIVERSE_CAD_TO_SIMREADY_STATE": manifest["paths"]["state_root"], } for name, entry in manifest.get("upstreams", {}).items(): env_name = UPSTREAMS.get(name, {}).get("env") @@ -1347,34 +2325,132 @@ def _dotenv_line_key(raw_line: str) -> str | None: return key or None +def _dotenv_line_pair(raw_line: str) -> tuple[str, str] | None: + line = raw_line.strip() + if not line or line.startswith("#") or "=" not in line: + return None + key, value = line.split("=", 1) + key = key.strip() + if key.startswith("export "): + key = key.removeprefix("export ").strip() + if not key or not (key[0].isalpha() or key[0] == "_"): + return None + if not all(character.isalnum() or character == "_" for character in key): + return None + value = value.strip() + if value and value[0] in {"'", '"'} and value[-1:] == value[0]: + value = value[1:-1] + elif " #" in value: + value = value.split(" #", 1)[0].rstrip() + return key, value + + def _read_dotenv_lines(path: Path) -> list[str]: return path.read_text(encoding="utf-8").splitlines() if path.is_file() else [] +def _load_content_agents_env_file(path: Path, *, extra_secret_names: tuple[str, ...] = ()) -> dict[str, Any]: + expanded = path.expanduser().resolve() + if not expanded.is_file(): + return _runtime_entry( + "blocked", + f"Content Agents private env file was not found: {expanded}", + path=str(expanded), + ) + + loaded: list[str] = [] + lines = _read_dotenv_lines(expanded) + allowed_names = set(CONTENT_AGENTS_SECRET_ENV_NAMES) | set(CONTENT_AGENTS_MODEL_ENV_NAMES) | set(extra_secret_names) + for line in lines: + parsed = _dotenv_line_pair(line) + if not parsed: + continue + key, value = parsed + if key not in allowed_names or not value: + continue + os.environ.setdefault(key, value) + loaded.append(key) + + custom_secret_names = { + value.strip() + for name in CONTENT_AGENTS_API_KEY_ENV_NAME_ENV_NAMES + for value in [os.environ.get(name, "")] + if value.strip() + } + custom_secret_names.update(name for name in extra_secret_names if name) + for line in lines: + parsed = _dotenv_line_pair(line) + if not parsed: + continue + key, value = parsed + if key not in custom_secret_names or not value: + continue + os.environ.setdefault(key, value) + loaded.append(key) + + return _runtime_entry( + "ready", + f"loaded {len(sorted(set(loaded)))} Content Agents environment name(s) from private env file", + path=str(expanded), + names=sorted(set(loaded)), + ) + + def _content_agents_secret_line(name: str, value: str) -> str: return f"{name}={value.replace(chr(10), '')}" -def _content_agents_secret_env() -> dict[str, str]: +def _content_agents_secret_env( + *, + model_options: dict[str, dict[str, str]] | None = None, + render_endpoint: str | None = None, +) -> dict[str, str]: available = { name: os.environ[name] for name in CONTENT_AGENTS_SECRET_ENV_NAMES if os.environ.get(name) } - if "NGC_API_KEY" not in available and os.environ.get("NVIDIA_API_KEY"): + for name in CONTENT_AGENTS_API_KEY_ENV_NAME_ENV_NAMES: + secret_name = os.environ.get(name, "").strip() + if secret_name: + secret_value = _read_credential_env(secret_name) + if secret_value: + available[secret_name] = secret_value + if model_options: + for group in model_options.values(): + secret_name = _parse_env_reference(group.get("api_key_env", "")) + secret_value = _read_credential_env(secret_name) + if secret_name and secret_value: + available[secret_name] = secret_value + if "NGC_API_KEY" not in available and _is_local_render_endpoint(render_endpoint): # The upstream local collection uses host.docker.internal for the local # renderer, while Material/Physics readiness treats non-local render - # hostnames as requiring the render usage key. Keep the public managed - # deployment contract to NVIDIA_API_KEY by mirroring it into the - # upstream private .env only when the explicit key is absent. - available["NGC_API_KEY"] = os.environ["NVIDIA_API_KEY"] + # hostnames as requiring the render usage key. Mirror whichever + # provider credential is actually configured into NGC_API_KEY so the + # render gate is satisfied, regardless of which VLM/LLM backend + # (nim, openai, anthropic, gemini, ...) is in use. + for name in CONTENT_AGENTS_PROVIDER_SECRET_ENV_NAMES: + if os.environ.get(name): + available["NGC_API_KEY"] = os.environ[name] + break + for name in CONTENT_AGENTS_MODEL_ENV_NAMES: + if os.environ.get(name): + available[name] = os.environ[name] return available -def _ensure_content_agents_secret_env(world_root: Path) -> Step: +def _ensure_content_agents_secret_env( + world_root: Path, + *, + model_options: dict[str, dict[str, str]] | None = None, + render_endpoint: str | None = None, +) -> Step: """Mirror known deployment secrets from the process env into upstream .env.""" env_path = world_root / ".env" - available = _content_agents_secret_env() + available = _content_agents_secret_env( + model_options=model_options, + render_endpoint=render_endpoint, + ) if not available: return Step( name="content_agents_secret_env", @@ -1395,9 +2471,18 @@ def _ensure_content_agents_secret_env(world_root: Path) -> Step: lines[index] = replacement changed = True appended: list[str] = [] - for name in CONTENT_AGENTS_SECRET_ENV_NAMES: + append_order = [ + *CONTENT_AGENTS_SECRET_ENV_NAMES, + *sorted( + set(available) + - set(CONTENT_AGENTS_SECRET_ENV_NAMES) + - set(CONTENT_AGENTS_MODEL_ENV_NAMES) + ), + ] + for name in append_order: if name in available and name not in existing_keys: appended.append(_content_agents_secret_line(name, available[name])) + existing_keys.add(name) if appended: if lines and lines[-1].strip(): lines.append("") @@ -1411,11 +2496,11 @@ def _ensure_content_agents_secret_env(world_root: Path) -> Step: except OSError: pass action = "updated" if changed else "already contains" - count = len([name for name in CONTENT_AGENTS_SECRET_ENV_NAMES if name in available]) + count = len(available) return Step( name="content_agents_secret_env", status="ready", - message=f"upstream Content Agents .env {action} {count} deployment credential name(s)", + message=f"upstream Content Agents .env {action} {count} deployment environment name(s)", ) @@ -1464,6 +2549,23 @@ def build_manifest(args: argparse.Namespace) -> tuple[dict[str, Any], dict[str, services: dict[str, Any] = {} env: dict[str, str] = {} + if "content-agents" in targets and args.content_agents_secret_env_file: + extra_secret_names = tuple( + name + for name in ( + args.content_agents_vlm_api_key_env, + args.content_agents_llm_api_key_env, + args.content_agents_image_gen_api_key_env, + ) + if name + ) + runtimes["content_agents_private_env"] = _load_content_agents_env_file( + args.content_agents_secret_env_file, + extra_secret_names=extra_secret_names, + ) + + content_agents_model_options = _content_agents_model_options(args) + request_runtime = _check_request_inputs(source_asset, output_root, check_only=args.check_only) runtimes["request"] = request_runtime @@ -1481,6 +2583,9 @@ def build_manifest(args: argparse.Namespace) -> tuple[dict[str, Any], dict[str, steps.extend(git_lfs_steps) if "conversion" in targets: + # usd-convert-cad conversion uses the pip wheel; the checkout is cloned + # only as its SKILL.md capability/guidance source. usd-convert-gsplat is + # a git-checkout runtime. upstreams_by_tool = { "usd-convert-cad": "usd_convert_cad", "usd-convert-gsplat": "usd_convert_gsplat", @@ -1491,9 +2596,9 @@ def build_manifest(args: argparse.Namespace) -> tuple[dict[str, Any], dict[str, steps.extend(upstream_steps) if "usd-convert-cad" in conversion_tools: cad_runtime, cad_steps = _check_usd_convert_cad( - Path(upstreams["usd_convert_cad"]["path"]), project_root=project_root, check_only=args.check_only, + skill_source=upstreams.get("usd_convert_cad"), ) runtimes["usd_convert_cad"] = cad_runtime steps.extend(cad_steps) @@ -1533,6 +2638,7 @@ def build_manifest(args: argparse.Namespace) -> tuple[dict[str, Any], dict[str, include_texture=args.include_texture, check_only=args.check_only, skip_deploy=args.skip_deploy, + model_options=content_agents_model_options, ) runtimes["content_agents"] = content_runtime steps.extend(content_steps) @@ -1546,13 +2652,7 @@ def build_manifest(args: argparse.Namespace) -> tuple[dict[str, Any], dict[str, env["RENDER_ENDPOINT"] = services["ovrtx"]["base_url"] env["OVRTX_RENDER_ENDPOINT"] = services["ovrtx"]["base_url"] - blockers: list[str] = [] - for name, entry in sorted(runtimes.items()): - if entry.get("status") == "blocked": - blockers.append(f"{name}: {entry.get('message')}") - for name, entry in sorted(services.items()): - if entry.get("status") == "blocked": - blockers.append(f"{name}: {entry.get('message')}") + blockers = _manifest_blockers(upstreams, runtimes, services) status = "ready" if not blockers else "blocked" if args.skip_content_agents and "content-agents" not in targets: runtimes["content_agents"] = _runtime_entry("skipped", "Content Agents preflight was explicitly skipped") @@ -1565,6 +2665,8 @@ def build_manifest(args: argparse.Namespace) -> tuple[dict[str, Any], dict[str, "manifest_path": str(manifest_path), "preflight_mode": "route-aware-dependency-bootstrap", "dependency_policy": "selected-target-dependencies", + "upstream_version_policy": "pinned-tested-integration", + "upstream_version_manifest": str(UPSTREAM_VERSION_MANIFEST_PATH), "platform": { "system": platform.system().lower(), "machine": platform.machine(), @@ -1613,6 +2715,19 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--skip-content-agents", action="store_true") parser.add_argument("--skip-deploy", action="store_true") parser.add_argument("--include-texture", action="store_true") + parser.add_argument("--content-agents-secret-env-file", type=Path, help="Optional private dotenv file with Content Agents provider credentials such as NVIDIA_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, or GEMINI_API_KEY.") + parser.add_argument("--content-agents-vlm-backend", default=None, help="Optional Content Agents VLM backend override, for example openai, anthropic, gemini, or nim.") + parser.add_argument("--content-agents-vlm-model", default=None, help="Optional Content Agents VLM model override, for example gpt-5.5, claude-sonnet-4-20250514, or gemini-3-pro-preview.") + parser.add_argument("--content-agents-vlm-endpoint", default=None, help="Optional Content Agents VLM OpenAI-compatible /v1 endpoint override; use openai for custom OpenAI-compatible endpoints and nim only for NIM endpoints.") + parser.add_argument("--content-agents-vlm-api-key-env", default=None, help="Optional environment variable name the VLM backend should read for its API key, for example CUSTOM_VLM_API_KEY.") + parser.add_argument("--content-agents-llm-backend", default=None, help="Optional Content Agents LLM backend override, for example openai, anthropic, gemini, or nim.") + parser.add_argument("--content-agents-llm-model", default=None, help="Optional Content Agents LLM model override, for example gpt-5.5, claude-sonnet-4-20250514, or gemini-3-pro-preview.") + parser.add_argument("--content-agents-llm-endpoint", default=None, help="Optional Content Agents LLM OpenAI-compatible /v1 endpoint override; use openai for custom OpenAI-compatible endpoints and nim only for NIM endpoints.") + parser.add_argument("--content-agents-llm-api-key-env", default=None, help="Optional environment variable name the LLM backend should read for its API key, for example CUSTOM_LLM_API_KEY.") + parser.add_argument("--content-agents-image-gen-backend", default=None, help="Optional Content Agents image-generation backend override, for example openai or nim.") + parser.add_argument("--content-agents-image-gen-model", default=None, help="Optional Content Agents image-generation model override, for example black-forest-labs/flux.2-klein-4b.") + parser.add_argument("--content-agents-image-gen-endpoint", default=None, help="Optional Content Agents image-generation OpenAI-compatible /v1 endpoint override; use openai for custom OpenAI-compatible endpoints and nim only for NIM endpoints.") + parser.add_argument("--content-agents-image-gen-api-key-env", default=None, help="Optional environment variable name the image-generation backend should read for its API key, for example MA_IMAGE_GEN_API_KEY or TA_IMAGE_GEN_API_KEY.") parser.add_argument("--lfs", action="store_true", help="Run git lfs install and git lfs pull for repo fixtures.") parser.add_argument("--no-update", action="store_true", help="Do not update existing upstream checkouts.") args = parser.parse_args(argv) diff --git a/skills/omniverse-cad-to-simready/references/publishing-layout.md b/skills/omniverse-cad-to-simready/references/publishing-layout.md new file mode 100644 index 00000000..22d792f9 --- /dev/null +++ b/skills/omniverse-cad-to-simready/references/publishing-layout.md @@ -0,0 +1,31 @@ +# CAD to SimReady Publishing Layout Notes + +Background on this skill's own file layout and frontmatter conventions. Read +this reference only when changing the skill's structure, frontmatter, or +publishing story; it is not needed to execute the workflow. + +## Source of Truth and Compatibility Aliases + +Use `skills/omniverse-cad-to-simready/` as the source of truth for this +product repo's skill. The `.agents/skills` symlink is a compatibility alias +for local agentskills.io-style discovery, and `.codex/skills` and +`.claude/skills` are agent-specific compatibility aliases. Edit the +`skills/` copy; the symlinked paths are not independent copies. + +## Frontmatter Field Placement + +Frontmatter keeps `version` and `tools` at top level for agentskills.io +runtime compatibility. NVCARPS discoverability fields live under `metadata` +(`metadata.author`, `metadata.tags`, `metadata.domain`, +`metadata.languages`) rather than at top level, so this skill satisfies both +the agentskills.io runtime contract and the repo's own catalog metadata +checks at once. + +## Nested References Tree + +The nested `references/` tree under this workflow is intentional. It keeps +one public catalog skill while retaining script-bearing atomic stage +references, upstream handoff notes, and router documentation (including this +file and `references/troubleshooting.md`) under the workflow. Do not flatten +those references or promote nested README references to sibling `SKILL.md` +files unless the repo's publishing model changes. diff --git a/skills/omniverse-cad-to-simready/references/simready-conform-profile/README.md b/skills/omniverse-cad-to-simready/references/simready-conform-profile/README.md index 856eb501..51ee27e8 100644 --- a/skills/omniverse-cad-to-simready/references/simready-conform-profile/README.md +++ b/skills/omniverse-cad-to-simready/references/simready-conform-profile/README.md @@ -6,13 +6,13 @@ Use this router reference after USD conversion and property assignment, before `simready-validate`. It chooses the SimReady feature-level conformance skill that should repair a failing profile requirement. -This reference ships a narrow `scripts/run.py` router for deterministic Skill -Hub handoff and report generation. It should not be treated as the source of +This reference ships a narrow `scripts/run.py` router for deterministic +workflow handoff and report generation. It should not be treated as the source of truth for canonical FET instructions. The source of truth is the SimReady Foundation repository: ```text -https://github.com/NVIDIA/simready-foundation/tree/main +https://github.com/NVIDIA/simready-foundation/tree/v2026.04.1 ``` Do not copy upstream FET skill instructions, requirement summaries, validators, @@ -27,13 +27,13 @@ use a local checkout and read the upstream `SKILL.md` file directly. - OpenUSD Python APIs (`pxr.Usd`, `pxr.UsdGeom`) for local helper scripts when they are used. - A SimReady Foundation checkout at - `${SIMREADY_FOUNDATION_ROOT:-$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/simready-foundation}` - or `$HOME/.physical-ai-skill-hub/upstreams/simready-foundation`, - checked out to branch `main`. + `${SIMREADY_FOUNDATION_ROOT:-$OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT/simready-foundation}` + or `$HOME/.omniverse-cad-to-simready/upstreams/simready-foundation`, + checked out to the exact ref in `upstream-versions.lock.json` (currently `v2026.04.1`). ## Scope -This reference owns only Skill Hub routing, report handoff, and local helper +This reference owns only Omniverse CAD-to-SimReady routing, report handoff, and local helper selection. Upstream SimReady Foundation owns feature-level conformance behavior. ## Upstream Skills @@ -62,22 +62,22 @@ Use this mapping when routing validation failures: The matching branch URLs are: ```text -https://github.com/NVIDIA/simready-foundation/blob/main/skills/simready-foundation-conform-fet-000-core/SKILL.md -https://github.com/NVIDIA/simready-foundation/blob/main/skills/simready-foundation-conform-fet-001-minimal/SKILL.md -https://github.com/NVIDIA/simready-foundation/blob/main/skills/simready-foundation-conform-fet-003-rigid-body-physics/SKILL.md -https://github.com/NVIDIA/simready-foundation/blob/main/skills/simready-foundation-conform-fet-004-simulate-multi-body-physics/SKILL.md -https://github.com/NVIDIA/simready-foundation/blob/main/skills/simready-foundation-conform-fet-005-simulate-grasp-physics/SKILL.md -https://github.com/NVIDIA/simready-foundation/blob/main/skills/simready-foundation-conform-fet-006-materials/SKILL.md -https://github.com/NVIDIA/simready-foundation/blob/main/skills/simready-foundation-conform-fet-007-nonvisual-materials/SKILL.md -https://github.com/NVIDIA/simready-foundation/blob/main/skills/simready-foundation-conform-fet-021-robot-core/SKILL.md -https://github.com/NVIDIA/simready-foundation/blob/main/skills/simready-foundation-conform-fet-023-robot-materials/SKILL.md -https://github.com/NVIDIA/simready-foundation/blob/main/skills/simready-foundation-conform-fet-024-base-articulation/SKILL.md +https://github.com/NVIDIA/simready-foundation/blob/v2026.04.1/skills/simready-foundation-conform-fet-000-core/SKILL.md +https://github.com/NVIDIA/simready-foundation/blob/v2026.04.1/skills/simready-foundation-conform-fet-001-minimal/SKILL.md +https://github.com/NVIDIA/simready-foundation/blob/v2026.04.1/skills/simready-foundation-conform-fet-003-rigid-body-physics/SKILL.md +https://github.com/NVIDIA/simready-foundation/blob/v2026.04.1/skills/simready-foundation-conform-fet-004-simulate-multi-body-physics/SKILL.md +https://github.com/NVIDIA/simready-foundation/blob/v2026.04.1/skills/simready-foundation-conform-fet-005-simulate-grasp-physics/SKILL.md +https://github.com/NVIDIA/simready-foundation/blob/v2026.04.1/skills/simready-foundation-conform-fet-006-materials/SKILL.md +https://github.com/NVIDIA/simready-foundation/blob/v2026.04.1/skills/simready-foundation-conform-fet-007-nonvisual-materials/SKILL.md +https://github.com/NVIDIA/simready-foundation/blob/v2026.04.1/skills/simready-foundation-conform-fet-021-robot-core/SKILL.md +https://github.com/NVIDIA/simready-foundation/blob/v2026.04.1/skills/simready-foundation-conform-fet-023-robot-materials/SKILL.md +https://github.com/NVIDIA/simready-foundation/blob/v2026.04.1/skills/simready-foundation-conform-fet-024-base-articulation/SKILL.md ``` ## Local Helper Policy Some legacy reference-local scripts remain only as narrow report-producing -helpers for the Skill Hub workflow. They are not the FET skill source of truth. +helpers for the Omniverse CAD-to-SimReady workflow. They are not the FET skill source of truth. Before running one, read the matching upstream Foundation skill and make sure the helper still matches the selected profile requirement. @@ -148,8 +148,8 @@ overwrite source files. Resolve the Foundation checkout before following any FET skill: ```bash -export PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT="${PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT:-$HOME/.physical-ai-skill-hub/upstreams}" -export SIMREADY_FOUNDATION_ROOT="${SIMREADY_FOUNDATION_ROOT:-$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/simready-foundation}" +export OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT="${OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT:-$HOME/.omniverse-cad-to-simready/upstreams}" +export SIMREADY_FOUNDATION_ROOT="${SIMREADY_FOUNDATION_ROOT:-$OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT/simready-foundation}" git -C "$SIMREADY_FOUNDATION_ROOT" checkout main ``` @@ -234,7 +234,7 @@ The workflow summary should include: | Symptom | Cause | Fix | |---------|-------|-----| -| Foundation skill file is missing | `SIMREADY_FOUNDATION_ROOT` points to the wrong checkout or branch | Check out `https://github.com/NVIDIA/simready-foundation` at `main`. | +| Foundation skill file is missing | `SIMREADY_FOUNDATION_ROOT` points to the wrong checkout or revision | Check out `https://github.com/NVIDIA/simready-foundation` at the exact ref in `upstream-versions.lock.json`. | | Sealed `.usdz` cannot be repaired in place | Feature repairs that author USD prims cannot rewrite a sealed package | Produce or request an unpacked USD-family asset before running prim-level repair. Core metadata repair may still proceed sidecar-only. | | `GSP.001` failure reported as final without classification | Vision-capable inspection or explicit grasp points were not available | Route to upstream `simready-foundation-conform-fet-005-simulate-grasp-physics`. If neither vision nor explicit points are available, report the FET005 step as `blocked`. | | `RB.MB.001` failure reported after Physics Agent | The USD has fewer than two `UsdPhysics.RigidBodyAPI` prims, even if it has many visual or collider prims | Route to upstream `simready-foundation-conform-fet-004-simulate-multi-body-physics` when there are at least two existing component colliders or part roots that represent source parts. If the asset has only one mesh component or one `GeomSubset` component, `simready-validate` treats `RB.MB.001` as non-blocking and preserves it under `ignored_issues`; do not invent geometry. | diff --git a/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_000_CORE/README.md b/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_000_CORE/README.md index 4e2f3d7f..62740a68 100644 --- a/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_000_CORE/README.md +++ b/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_000_CORE/README.md @@ -5,20 +5,20 @@ Source of truth: ```text -https://github.com/NVIDIA/simready-foundation/blob/main/skills/simready-foundation-conform-fet-000-core/SKILL.md +https://github.com/NVIDIA/simready-foundation/blob/v2026.04.1/skills/simready-foundation-conform-fet-000-core/SKILL.md ``` Use an authenticated local checkout at `$SIMREADY_FOUNDATION_ROOT/skills/simready-foundation-conform-fet-000-core/SKILL.md` or -`$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/simready-foundation/skills/simready-foundation-conform-fet-000-core/SKILL.md` +`$OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT/simready-foundation/skills/simready-foundation-conform-fet-000-core/SKILL.md` when browser access is unavailable. Do not copy FET000 requirement summaries or repair policy into this repo. ## Local Helper -This directory only keeps a legacy Skill Hub helper script for deterministic +This directory only keeps a legacy Omniverse CAD-to-SimReady helper script for deterministic metadata updates and JSON reports: ```bash diff --git a/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_000_CORE/scripts/run.py b/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_000_CORE/scripts/run.py index 18cad5b4..2747b3c9 100644 --- a/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_000_CORE/scripts/run.py +++ b/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_000_CORE/scripts/run.py @@ -233,7 +233,7 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--profile", default="Prop-Robotics-Neutral") parser.add_argument("--profile-version", default="1.0.0") parser.add_argument("--source-asset") - parser.add_argument("--generated-by", default="physical-ai-skill-hub") + parser.add_argument("--generated-by", default="omniverse-cad-to-simready") parser.add_argument("--author") parser.add_argument("--tag", dest="tags", action="append", default=[]) parser.add_argument("--pipeline-step", dest="pipeline_steps", action="append", default=[]) diff --git a/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_001_MINIMAL/README.md b/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_001_MINIMAL/README.md index 2b743798..56515462 100644 --- a/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_001_MINIMAL/README.md +++ b/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_001_MINIMAL/README.md @@ -5,20 +5,20 @@ Source of truth: ```text -https://github.com/NVIDIA/simready-foundation/blob/main/skills/simready-foundation-conform-fet-001-minimal/SKILL.md +https://github.com/NVIDIA/simready-foundation/blob/v2026.04.1/skills/simready-foundation-conform-fet-001-minimal/SKILL.md ``` Use an authenticated local checkout at `$SIMREADY_FOUNDATION_ROOT/skills/simready-foundation-conform-fet-001-minimal/SKILL.md` or -`$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/simready-foundation/skills/simready-foundation-conform-fet-001-minimal/SKILL.md` +`$OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT/simready-foundation/skills/simready-foundation-conform-fet-001-minimal/SKILL.md` when browser access is unavailable. Do not copy FET001 requirement summaries or repair policy into this repo. ## Local Helper -This directory only keeps a legacy Skill Hub helper script for deterministic +This directory only keeps a legacy Omniverse CAD-to-SimReady helper script for deterministic unit normalization and JSON reports: ```bash diff --git a/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_001_MINIMAL/scripts/run.py b/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_001_MINIMAL/scripts/run.py index c9937808..2893921e 100644 --- a/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_001_MINIMAL/scripts/run.py +++ b/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_001_MINIMAL/scripts/run.py @@ -20,7 +20,7 @@ SKILL = "FET_001_MINIMAL" TOOL = "pxr.Usd/UsdGeom unit normalization" -AUTHORING_METADATA = "physical-ai-skill-hub FET_001_MINIMAL/scripts/run.py v0.1.0" +AUTHORING_METADATA = "omniverse-cad-to-simready FET_001_MINIMAL/scripts/run.py v0.1.0" SUPPORTED_USD_EXTENSIONS = {".usd", ".usda", ".usdc"} DEFAULT_PROFILE = "Prop-Robotics-Neutral" DEFAULT_PROFILE_VERSION = "1.0.0" diff --git a/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_004_SIMULATE_MULTI_BODY_PHYSICS/README.md b/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_004_SIMULATE_MULTI_BODY_PHYSICS/README.md index c0f1f66d..b671aa45 100644 --- a/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_004_SIMULATE_MULTI_BODY_PHYSICS/README.md +++ b/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_004_SIMULATE_MULTI_BODY_PHYSICS/README.md @@ -5,20 +5,20 @@ Source of truth: ```text -https://github.com/NVIDIA/simready-foundation/blob/main/skills/simready-foundation-conform-fet-004-simulate-multi-body-physics/SKILL.md +https://github.com/NVIDIA/simready-foundation/blob/v2026.04.1/skills/simready-foundation-conform-fet-004-simulate-multi-body-physics/SKILL.md ``` Use an authenticated local checkout at `$SIMREADY_FOUNDATION_ROOT/skills/simready-foundation-conform-fet-004-simulate-multi-body-physics/SKILL.md` or -`$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/simready-foundation/skills/simready-foundation-conform-fet-004-simulate-multi-body-physics/SKILL.md` +`$OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT/simready-foundation/skills/simready-foundation-conform-fet-004-simulate-multi-body-physics/SKILL.md` when browser access is unavailable. Do not copy FET004 requirement summaries or repair policy into this repo. ## Local Helper -This directory only keeps a legacy Skill Hub helper script for promoting +This directory only keeps a legacy Omniverse CAD-to-SimReady helper script for promoting existing component rigid bodies and writing JSON reports: ```bash diff --git a/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_005_SIMULATE_GRASP_PHYSICS/README.md b/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_005_SIMULATE_GRASP_PHYSICS/README.md index 759ef1d3..a30a1821 100644 --- a/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_005_SIMULATE_GRASP_PHYSICS/README.md +++ b/skills/omniverse-cad-to-simready/references/simready-conform-profile/references/FET_005_SIMULATE_GRASP_PHYSICS/README.md @@ -5,13 +5,13 @@ Source of truth: ```text -https://github.com/NVIDIA/simready-foundation/blob/main/skills/simready-foundation-conform-fet-005-simulate-grasp-physics/SKILL.md +https://github.com/NVIDIA/simready-foundation/blob/v2026.04.1/skills/simready-foundation-conform-fet-005-simulate-grasp-physics/SKILL.md ``` Use an authenticated local checkout at `$SIMREADY_FOUNDATION_ROOT/skills/simready-foundation-conform-fet-005-simulate-grasp-physics/SKILL.md` or -`$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/simready-foundation/skills/simready-foundation-conform-fet-005-simulate-grasp-physics/SKILL.md` +`$OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT/simready-foundation/skills/simready-foundation-conform-fet-005-simulate-grasp-physics/SKILL.md` when browser access is unavailable. Do not copy FET005 requirement summaries, visual policy, or repair policy into @@ -19,7 +19,7 @@ this repo. ## Local Helper -This directory only keeps a legacy Skill Hub helper script for deterministic +This directory only keeps a legacy Omniverse CAD-to-SimReady helper script for deterministic grasp-line authoring and JSON reports. Prefer the upstream Foundation script when it is available: @@ -33,7 +33,7 @@ uv run --python 3.12 python "$SIMREADY_FOUNDATION_ROOT/skills/simready-foundatio --report /author-grasp-line.json ``` -Use the local helper only when the installed Skill Hub workflow needs its +Use the local helper only when the installed Omniverse CAD-to-SimReady workflow needs its existing report contract: ```bash diff --git a/skills/omniverse-cad-to-simready/references/simready-conform-profile/scripts/check_dependencies.py b/skills/omniverse-cad-to-simready/references/simready-conform-profile/scripts/check_dependencies.py index 2c5e8770..8b9478f7 100644 --- a/skills/omniverse-cad-to-simready/references/simready-conform-profile/scripts/check_dependencies.py +++ b/skills/omniverse-cad-to-simready/references/simready-conform-profile/scripts/check_dependencies.py @@ -14,6 +14,8 @@ from script_utils import check_result as _check, emit_json_report +from preflight_manifest import preflight_required, preflight_status_check + SKILL = "simready-conform-profile" HELPER_SCRIPTS = [ @@ -29,6 +31,13 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--report", type=Path) args = parser.parse_args(argv) + if preflight_required(): + preflight_check = preflight_status_check(SKILL, "openusd_python") + if not preflight_check["passed"]: + payload = {"skill": SKILL, "passed": False, "checks": [preflight_check], "errors": [preflight_check["message"]]} + emit_json_report(payload, args.report) + return 1 + skill_root = Path(__file__).resolve().parents[1] checks = [_check("python_available", True, f"Python executable: {sys.executable}")] for relative in HELPER_SCRIPTS: diff --git a/skills/omniverse-cad-to-simready/references/simready-conform-profile/scripts/run.py b/skills/omniverse-cad-to-simready/references/simready-conform-profile/scripts/run.py index 15ba2e9b..dbebdf39 100644 --- a/skills/omniverse-cad-to-simready/references/simready-conform-profile/scripts/run.py +++ b/skills/omniverse-cad-to-simready/references/simready-conform-profile/scripts/run.py @@ -16,6 +16,8 @@ from script_utils import emit_json_report +from preflight_manifest import preflight_required, preflight_status_check + SKILL = "simready-conform-profile" DEFAULT_PROFILE = "Prop-Robotics-Neutral" @@ -371,6 +373,11 @@ def conform(args: argparse.Namespace) -> dict[str, Any]: asset_path = args.asset_path.resolve() output_dir = args.output_dir.resolve() report = _empty_report(asset_path, output_dir, args) + if preflight_required(): + preflight_check = preflight_status_check(SKILL, "openusd_python") + if not preflight_check["passed"]: + report["errors"].append(preflight_check["message"]) + return report if not asset_path.exists(): report["errors"].append(f"Asset path does not exist: {asset_path}") return report diff --git a/skills/omniverse-cad-to-simready/references/simready-validate/README.md b/skills/omniverse-cad-to-simready/references/simready-validate/README.md index ca15b84f..5e7e0edf 100644 --- a/skills/omniverse-cad-to-simready/references/simready-validate/README.md +++ b/skills/omniverse-cad-to-simready/references/simready-validate/README.md @@ -28,8 +28,8 @@ Require: direct legacy discovery. When `PHYSICAL_AI_REQUIRE_PREFLIGHT=1` is set, missing profile-validation readiness blocks at the preflight guardrail. - `simready.validate` / `simready-validate` from NVIDIA SimReady Foundation, or a source checkout with `requirements.txt` or `nv_core/validator_sample/requirements.txt` -- Upstream source: `https://github.com/NVIDIA/simready-foundation` on branch `main` -- Temporary aarch64 OpenUSD runtime fallback: NVIDIA OpenUSD Exchange SDK package `usd-exchange>=2.3.0` from `https://github.com/NVIDIA-Omniverse/usd-exchange` +- Upstream source: `https://github.com/NVIDIA/simready-foundation` pinned by `upstream-versions.lock.json` to `v2026.04.1` +- Deterministic OpenUSD runtime: NVIDIA OpenUSD Exchange SDK package `usd-exchange==2.3.0` from `https://github.com/NVIDIA-Omniverse/usd-exchange`, with the complete SimReady venv package set pinned in the same manifest - SimReady Foundation spec files: `capabilities/`, `features/`, and `profiles/profiles.toml` Check installed reference dependencies with: @@ -38,13 +38,19 @@ Check installed reference dependencies with: python3 scripts/check_dependencies.py --report dependency-check.json ``` -If `--foundation-root`, `--foundation-spec-root`, `SIMREADY_FOUNDATION_ROOT`, and `SIMREADY_FOUNDATION_SPEC_ROOT` are not configured and no installed `simready.validate` specs are available, provide a checkout under `$HOME/.physical-ai-skill-hub/upstreams/simready-foundation` or `$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/simready-foundation`, checked out to `main`, and load `nv_core/sr_specs/docs` plus `nv_core/validator_sample` from that checkout. +If `--foundation-root`, `--foundation-spec-root`, `SIMREADY_FOUNDATION_ROOT`, and `SIMREADY_FOUNDATION_SPEC_ROOT` are not configured and no installed `simready.validate` specs are available, provide a checkout under `$HOME/.omniverse-cad-to-simready/upstreams/simready-foundation` or `$OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT/simready-foundation`, checked out to the manifest pin (`v2026.04.1`), and load `nv_core/sr_specs/docs` plus `nv_core/validator_sample` from that checkout. -If `simready-validate` is not on `PATH`, do not stop there. `scripts/run.py` must install the runtime from the Foundation checkout's root `requirements.txt` when present, otherwise from `nv_core/validator_sample/requirements.txt`, into a dedicated venv and use that executable. Override the venv with `PHYSICAL_AI_SIMREADY_VALIDATE_VENV`; otherwise the default is `$XDG_CACHE_HOME/physical-ai-skill-hub/simready-validate-venv` or `$HOME/.cache/physical-ai-skill-hub/simready-validate-venv`. +If `simready-validate` is not on `PATH`, do not stop there. `scripts/run.py` installs the exact runtime set from `upstream-versions.lock.json` into a dedicated venv and verifies the installed distribution versions before using that executable. Override the venv with `PHYSICAL_AI_SIMREADY_VALIDATE_VENV`; otherwise the default is `$XDG_CACHE_HOME/omniverse-cad-to-simready/simready-validate-venv` or `$HOME/.cache/omniverse-cad-to-simready/simready-validate-venv`. -Until the upstream Foundation dependency metadata is fixed, Linux aarch64 hosts need one extra guardrail: PyPI `usd-core` is not available for this architecture, while `usd-exchange` ships the required OpenUSD Python modules and shared libraries for aarch64. If the normal Foundation `requirements.txt` install fails because `usd-core` cannot resolve, `scripts/run.py` must retry in the same dedicated venv by installing `usd-exchange>=2.3.0`, `omniverse-asset-validator`, `omniverse-usd-profiles`, the non-`simready-validate` Foundation requirements such as `numpy`, and then `simready-validate` itself with `--no-deps`. Do not report `BLOCKED` for the aarch64 `usd-core` resolver failure until this USD Exchange SDK fallback has also failed. +This locked runtime is also the Linux aarch64 solution: `usd-exchange==2.3.0` +provides OpenUSD Python modules and shared libraries, and +`simready-validate==2026.4.8` is installed with `--no-deps`. The manifest pins +`omniverse-asset-validator==1.18.0`, +`omniverse-usd-profiles==1.10.22`, and every transitive package used by that +venv, preventing a future resolver run from silently selecting an incompatible +combination. -Do not fall back to local profile presets or direct `omni_asset_validate` feature/capability flags for validation. Report `BLOCKED` only when the executable is unavailable, no usable Foundation checkout/spec root exists for installation and validation, or both the normal Foundation install and the aarch64 USD Exchange SDK fallback fail. +Do not fall back to local profile presets or direct `omni_asset_validate` feature/capability flags for validation. Report `BLOCKED` when the executable is unavailable, no usable Foundation checkout/spec root exists, the locked runtime cannot be installed, or the installed versions do not match the manifest. ## Target Selection @@ -81,8 +87,8 @@ For URDF or MuJoCo robot assets, prefer `Robot-Body-Runnable` unless the user na validation-only or property assignment was skipped, record that exception. 3. Confirm earlier validation has passed, or state that minimum USD and generic Asset Validator checks should run first. 4. Select a formal SimReady Foundation profile from user intent and asset type. -5. Resolve the SimReady Foundation source checkout from `--foundation-root` or `SIMREADY_FOUNDATION_ROOT`; alternatively resolve specs from `--foundation-spec-root` or `SIMREADY_FOUNDATION_SPEC_ROOT`. If no path is configured, use `$PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/simready-foundation` or `$HOME/.physical-ai-skill-hub/upstreams/simready-foundation`, checked out to `main`. -6. Run this reference's portable `scripts/run.py`, which installs `simready-validate` from the Foundation checkout when the CLI is missing on `PATH`, uses the temporary USD Exchange SDK runtime fallback on Linux aarch64 when PyPI `usd-core` cannot resolve, then uses Foundation `simready-validate`/`validator_sample` behavior to load Foundation `capabilities`, `features`, and `profiles/profiles.toml`. +5. Resolve the SimReady Foundation source checkout from `--foundation-root` or `SIMREADY_FOUNDATION_ROOT`; alternatively resolve specs from `--foundation-spec-root` or `SIMREADY_FOUNDATION_SPEC_ROOT`. If no path is configured, use `$OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT/simready-foundation` or `$HOME/.omniverse-cad-to-simready/upstreams/simready-foundation`, checked out to the ref pinned in `upstream-versions.lock.json`. +6. Run this reference's portable `scripts/run.py`, which installs and verifies the locked SimReady runtime when the CLI is missing on `PATH`, then uses Foundation `simready-validate` behavior to load Foundation `capabilities`, `features`, and `profiles/profiles.toml`. 7. Parse profile, feature, requirement, issue, warning, and error results from the Foundation validation runtime. 8. Inspect the asset topology with OpenUSD. Treat `RB.MB.001` as non-blocking when the asset has only one mesh component or one `GeomSubset` component, because there is no reusable multi-body component structure to promote. Preserve the ignored issue under `ignored_issues`, add a warning, and pass the profile if no other failures remain. 9. Fail when any selected profile feature fails or any issue has `ERROR` or `FAILURE` severity after applying the single-component `RB.MB.001` policy. @@ -169,4 +175,4 @@ Use this handoff: |---|---| | Passes selected profile | Report validation result and preserve the JSON report. | | Fails selected profile feature | Send issues to a post-assignment repair loop through `simready-conform-profile`, then rerun this reference on the newest authored USD. | -| SimReady Foundation runtime blocked | Provide a `simready-foundation` checkout on branch `main` with `--foundation-root` or `SIMREADY_FOUNDATION_ROOT`, then retry so `scripts/run.py` can install the compatible runtime from `requirements.txt`; on Linux aarch64, confirm the USD Exchange SDK fallback was attempted after any `usd-core` resolver failure. | +| SimReady Foundation runtime blocked | Provide the manifest-pinned `simready-foundation` checkout with `--foundation-root` or `SIMREADY_FOUNDATION_ROOT`, then retry so `scripts/run.py` can install and verify the exact runtime from `upstream-versions.lock.json`. | diff --git a/skills/omniverse-cad-to-simready/references/simready-validate/scripts/check_dependencies.py b/skills/omniverse-cad-to-simready/references/simready-validate/scripts/check_dependencies.py index 02ebf689..b9e93fc8 100644 --- a/skills/omniverse-cad-to-simready/references/simready-validate/scripts/check_dependencies.py +++ b/skills/omniverse-cad-to-simready/references/simready-validate/scripts/check_dependencies.py @@ -52,12 +52,12 @@ def _default_foundation_root() -> Path | None: env_root = os.environ.get("SIMREADY_FOUNDATION_ROOT") if env_root: return Path(env_root).expanduser().resolve() - upstream_root = os.environ.get("PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT") + upstream_root = os.environ.get("OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT") if upstream_root: candidate = Path(upstream_root).expanduser() / DEFAULT_FOUNDATION_CHECKOUT if candidate.exists(): return candidate.resolve() - candidate = Path.home() / ".physical-ai-skill-hub" / "upstreams" / DEFAULT_FOUNDATION_CHECKOUT + candidate = Path.home() / ".omniverse-cad-to-simready" / "upstreams" / DEFAULT_FOUNDATION_CHECKOUT if candidate.exists(): return candidate.resolve() return None diff --git a/skills/omniverse-cad-to-simready/references/simready-validate/scripts/run.py b/skills/omniverse-cad-to-simready/references/simready-validate/scripts/run.py index 4705a589..791fd801 100644 --- a/skills/omniverse-cad-to-simready/references/simready-validate/scripts/run.py +++ b/skills/omniverse-cad-to-simready/references/simready-validate/scripts/run.py @@ -7,7 +7,6 @@ import argparse import json import os -import platform from pathlib import Path import re import shutil @@ -20,6 +19,15 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "shared")) from script_utils import emit_json_report +from upstream_versions import ( + UPSTREAM_VERSION_MANIFEST_PATH, + expected_versions, + git_upstreams, + runtime_package_names, + runtime_requirements, + version_mismatches, + version_probe_code, +) from preflight_manifest import ( load_preflight_manifest, @@ -37,19 +45,16 @@ TOOL = "simready-validate" DEFAULT_PROFILE = "Prop-Robotics-Neutral" DEFAULT_PROFILE_VERSION = "1.0.0" -DEFAULT_UPSTREAM_ROOT = Path.home() / ".physical-ai-skill-hub" / "upstreams" +DEFAULT_UPSTREAM_ROOT = Path.home() / ".omniverse-cad-to-simready" / "upstreams" DEFAULT_FOUNDATION_REPO_URL = "https://github.com/NVIDIA/simready-foundation" -DEFAULT_FOUNDATION_BRANCH = "main" -DEFAULT_SIMREADY_VALIDATE_REQUIREMENT = "simready-validate>=2026.4.8" +DEFAULT_FOUNDATION_BRANCH = git_upstreams()["simready_foundation"]["ref"] NONBLOCKING_SINGLE_COMPONENT_REQUIREMENT = "RB.MB.001" -SIMREADY_RUNTIME_EXTRA_REQUIREMENTS = [ - "numpy>=1.24,<3", -] -USD_EXCHANGE_SDK_FALLBACK_REQUIREMENTS = [ - "usd-exchange>=2.3.0", - "omniverse-asset-validator", - "omniverse-usd-profiles>=1.10.22", -] +SIMREADY_RUNTIME_REQUIREMENTS = runtime_requirements("simready_validate") +SIMREADY_NO_DEPS_REQUIREMENTS = runtime_requirements("simready_validate", no_deps=True) +SIMREADY_RUNTIME_PACKAGE_NAMES = ( + *runtime_package_names("simready_validate"), + *runtime_package_names("simready_validate", no_deps=True), +) def _checkout_name_from_repo_url(repo_url: str) -> str: @@ -320,7 +325,7 @@ def _runtime_venv_dir() -> Path: if venv_dir is not None: return venv_dir cache_home = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")).expanduser() - return (cache_home / "physical-ai-skill-hub" / "simready-validate-venv").resolve() + return (cache_home / "omniverse-cad-to-simready" / "simready-validate-venv").resolve() def _venv_bin_dir(venv_dir: Path) -> Path: @@ -337,40 +342,29 @@ def _venv_executable(venv_dir: Path) -> Path: def _runtime_dependencies_ready(python: Path) -> bool: + return _runtime_version_error(python) is None + + +def _runtime_version_error(python: Path) -> str | None: + code = ( + "import numpy; import omni.asset_validator; from pxr import Usd; " + + version_probe_code(SIMREADY_RUNTIME_PACKAGE_NAMES) + ) completed = subprocess.run( - [str(python), "-c", "import numpy"], + [str(python), "-c", code], capture_output=True, text=True, - timeout=60, + timeout=120, check=False, ) - return completed.returncode == 0 - - -def _dedupe_requirements(requirements: list[str]) -> list[str]: - seen: set[str] = set() - deduped: list[str] = [] - for requirement in requirements: - key = requirement.strip().lower() - if not key or key in seen: - continue - seen.add(key) - deduped.append(requirement) - return deduped - - -def _foundation_requirements(requirements_path: Path) -> tuple[list[str], list[str]]: - simready_requirements: list[str] = [] - other_requirements: list[str] = [] - for raw_line in requirements_path.read_text(encoding="utf-8").splitlines(): - line = raw_line.split("#", 1)[0].strip() - if not line: - continue - if line.lower().startswith("simready-validate"): - simready_requirements.append(line) - elif not line.lower().startswith("usd-core"): - other_requirements.append(line) - return simready_requirements or [DEFAULT_SIMREADY_VALIDATE_REQUIREMENT], other_requirements + if completed.returncode != 0: + return completed.stderr.strip() or completed.stdout.strip() or f"version probe exited {completed.returncode}" + try: + installed = json.loads(completed.stdout.strip().splitlines()[-1]) + except (IndexError, json.JSONDecodeError) as exc: + return f"version probe did not return JSON: {exc}" + mismatches = version_mismatches(expected_versions(SIMREADY_RUNTIME_PACKAGE_NAMES), installed) + return "; ".join(mismatches) if mismatches else None def _foundation_requirements_path(root: Path) -> Path: @@ -381,17 +375,6 @@ def _foundation_requirements_path(root: Path) -> Path: return next((path for path in candidates if path.is_file()), candidates[0]) -def _is_aarch64() -> bool: - return platform.machine().lower() in {"aarch64", "arm64"} - - -def _should_try_usd_exchange_fallback(install_detail: str) -> bool: - if not _is_aarch64(): - return False - lowered = install_detail.lower() - return "usd-core" in lowered or "no matching distribution" in lowered or "resolutionimpossible" in lowered - - def _run_pip_install(python: Path, args: list[str], *, timeout: int = 900) -> subprocess.CompletedProcess[str]: return subprocess.run( [str(python), "-m", "pip", "install", "--disable-pip-version-check", *args], @@ -402,24 +385,21 @@ def _run_pip_install(python: Path, args: list[str], *, timeout: int = 900) -> su ) -def _install_cli_with_usd_exchange_sdk_runtime( - python: Path, - executable: Path, - requirements_path: Path, -) -> str | None: - simready_requirements, other_requirements = _foundation_requirements(requirements_path) - runtime_requirements = _dedupe_requirements([*USD_EXCHANGE_SDK_FALLBACK_REQUIREMENTS, *other_requirements, *SIMREADY_RUNTIME_EXTRA_REQUIREMENTS]) - runtime_install = _run_pip_install(python, runtime_requirements) +def _install_cli_with_locked_runtime(python: Path, executable: Path) -> str | None: + runtime_install = _run_pip_install(python, list(SIMREADY_RUNTIME_REQUIREMENTS)) if runtime_install.returncode != 0: detail = runtime_install.stderr.strip() or runtime_install.stdout.strip() - return f"Failed to install USD Exchange SDK fallback runtime: {detail}" + return f"Failed to install pinned SimReady runtime from {UPSTREAM_VERSION_MANIFEST_PATH}: {detail}" - simready_install = _run_pip_install(python, ["--no-deps", *simready_requirements]) + simready_install = _run_pip_install(python, ["--no-deps", *SIMREADY_NO_DEPS_REQUIREMENTS]) if simready_install.returncode != 0: detail = simready_install.stderr.strip() or simready_install.stdout.strip() - return f"Failed to install {TOOL} with USD Exchange SDK fallback runtime: {detail}" + return f"Failed to install pinned {TOOL} runtime: {detail}" if not executable.is_file(): - return f"Installed USD Exchange SDK fallback runtime, but {executable} was not created" + return f"Installed pinned SimReady runtime, but {executable} was not created" + version_error = _runtime_version_error(python) + if version_error: + return f"Pinned SimReady runtime verification failed: {version_error}" return None @@ -428,8 +408,8 @@ def _install_cli_from_foundation_repo(foundation_root: Path | None) -> tuple[str if root is None: return None, ( f"{TOOL} CLI was not found on PATH, and no SimReady Foundation checkout was found at " - f"$SIMREADY_FOUNDATION_ROOT, $PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT/{DEFAULT_FOUNDATION_CHECKOUT}, " - f"or $HOME/.physical-ai-skill-hub/upstreams/{DEFAULT_FOUNDATION_CHECKOUT} " + f"$SIMREADY_FOUNDATION_ROOT, $OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT/{DEFAULT_FOUNDATION_CHECKOUT}, " + f"or $HOME/.omniverse-cad-to-simready/upstreams/{DEFAULT_FOUNDATION_CHECKOUT} " f"checked out to {DEFAULT_FOUNDATION_BRANCH}" ) requirements_path = _foundation_requirements_path(root) @@ -454,17 +434,9 @@ def _install_cli_from_foundation_repo(foundation_root: Path | None) -> tuple[str return None, f"Failed to create {TOOL} runtime venv at {venv_dir}: {detail}" python = _venv_python(venv_dir) - install = _run_pip_install(python, ["-r", str(requirements_path), *SIMREADY_RUNTIME_EXTRA_REQUIREMENTS]) - if install.returncode != 0: - detail = install.stderr.strip() or install.stdout.strip() - if _should_try_usd_exchange_fallback(detail): - fallback_error = _install_cli_with_usd_exchange_sdk_runtime(python, executable, requirements_path) - if fallback_error is None: - return str(executable), None - return None, f"Failed to install {TOOL} from {requirements_path}: {detail}\n{fallback_error}" - return None, f"Failed to install {TOOL} from {requirements_path}: {detail}" - if not executable.is_file(): - return None, f"Installed {requirements_path}, but {executable} was not created" + install_error = _install_cli_with_locked_runtime(python, executable) + if install_error: + return None, install_error return str(executable), None @@ -598,7 +570,7 @@ def _resolve_foundation_root(explicit: Path | None) -> Path | None: env_value = os.getenv("SIMREADY_FOUNDATION_ROOT") if env_value: return Path(env_value) - upstream_root = Path(os.getenv("PHYSICAL_AI_SKILL_HUB_UPSTREAM_ROOT", str(DEFAULT_UPSTREAM_ROOT))) + upstream_root = Path(os.getenv("OMNIVERSE_CAD_TO_SIMREADY_UPSTREAM_ROOT", str(DEFAULT_UPSTREAM_ROOT))) candidate = upstream_root / DEFAULT_FOUNDATION_CHECKOUT return candidate if candidate.exists() else None diff --git a/skills/omniverse-cad-to-simready/references/troubleshooting.md b/skills/omniverse-cad-to-simready/references/troubleshooting.md new file mode 100644 index 00000000..7ef2a882 --- /dev/null +++ b/skills/omniverse-cad-to-simready/references/troubleshooting.md @@ -0,0 +1,51 @@ +# CAD to SimReady Troubleshooting + +Stage-local symptom/cause/fix detail for the `omniverse-cad-to-simready` +router. Read this reference only when a specific stage is failing or a +validation gate reports a requirement the router's Hard Rules do not spell out +in full. The router (`SKILL.md`) keeps the invariants; this file keeps the +supporting detail. + +## Symptom / Cause / Fix Table + +| Symptom | Cause | Fix | +|---------|-------|-----| +| Downstream reference reports that cad-to-simready preflight has not prepared a component | `PHYSICAL_AI_REQUIRE_PREFLIGHT=1` is set, but the manifest is missing or the required runtime/service is not `ready` | Run `preflight/scripts/preflight.py`, source the generated env file, or explicitly disable service deployment with `--skip-content-agents` only when Content Agents are out of scope. | +| Workflow stops on `GSP.001` and reports the failure as unclassified | Visual evidence or explicit grasp points were not provided to FET005 | Run upstream `simready-foundation-conform-fet-005-simulate-grasp-physics` only after a vision-capable agent has reviewed the asset, or pass explicit grasp points. Otherwise report the FET005 step as `blocked`, not failed. | +| Validation fails after a meaningful USD artifact already exists | Workflow stopped at the first validation finding | Continue remaining diagnostic gates and mark the result `needs_rerun`. Do not stop at validation findings once a USD artifact has been produced. | +| Property-assignment stage fails with a missing service endpoint | Content Agents service was not deployed before conversion | Run `deploy-content-agents` first. Do not start asset inspection, conversion, validation, conformance, rendering, or packaging before Content Agents readiness when property assignment will run. | +| Material Agent reports that rendering produced `0 images` after unit or profile repair | A FET repair, commonly FET001 unit normalization, was applied before Material Agent and changed the USD layering/scene state consumed by the service | Rerun assignment from the converted/minimum-valid USD: Material Agent first, then Physics Agent, then run `simready-conform-profile` and FET repairs on the latest service-authored USD. | +| Material or Physics Agent local optimized path reports `Permission denied: '/app/.build-resources/scene_optimizer_core/python'` | Local Docker Scene Optimizer bundle permissions prevent the non-root service user from reading the packaged SO runtime | Repair the relevant local container with `docker exec --user root content-material-agent-service chmod -R a+rX /app/.build-resources/scene_optimizer_core` or `docker exec --user root content-physics-agent-service chmod -R a+rX /app/.build-resources/scene_optimizer_core`, then rerun the same optimized agent command. Do not treat the no-optimizer fallback as the root cause for instanced/prototype assets. | +| `RB.MB.001` fails even though the asset has many prims | The profile counts `UsdPhysics.RigidBodyAPI` prims, not visual or collider prims; Physics Agent may author one root rigid body | Route to upstream `simready-foundation-conform-fet-004-simulate-multi-body-physics`. First ensure Physics Agent used composed-topology optimization when applicable, then promote existing component colliders/part roots when the active profile reports FET004/RB.MB.001 and no geometry must be invented. | + +## FET Repair Routing Detail + +`simready-conform-profile` routes feature repair to upstream SimReady +Foundation FET skills such as `simready-foundation-conform-fet-000-core`, +`simready-foundation-conform-fet-001-minimal`, +`simready-foundation-conform-fet-004-simulate-multi-body-physics`, and +`simready-foundation-conform-fet-005-simulate-grasp-physics` from branch +`main`. Use it only after property assignment when +`property_assignment_intent=run`. + +If `simready-validate` reports a repairable requirement after the first +conformance pass, feed the structured requirement IDs back into the +`simready-conform-profile` reference before writing the final result. + +- `GSP.001` is owned by upstream + `simready-foundation-conform-fet-005-simulate-grasp-physics`; run that skill + when a vision-capable agent can inspect visual evidence or explicit grasp + points were provided, otherwise record the FET005 step as blocked by missing + vision/points instead of treating it as an optional preview task. +- For `RB.MB.001`, route the failure to upstream + `simready-foundation-conform-fet-004-simulate-multi-body-physics`. Do not + assume multiple visual prims are multiple rigid bodies; inspect + `UsdPhysics.RigidBodyAPI` applications. When the Physics Agent report shows + composed topology optimization or the USD has existing component + colliders/part roots and the profile validator reports FET004/RB.MB.001, + FET004 should promote those existing components into rigid bodies without + creating geometry. Do not mark the gate not applicable until after + confirming there are fewer than two reusable body candidates. +- Upstream `simready-foundation-conform-fet-005-simulate-grasp-physics` needs + visual review or explicit grasp points before it can author a meaningful + grasp vector. diff --git a/skills/omniverse-cad-to-simready/references/validate-usd-minimum/scripts/check_dependencies.py b/skills/omniverse-cad-to-simready/references/validate-usd-minimum/scripts/check_dependencies.py index 005a75b1..c3d5db3f 100644 --- a/skills/omniverse-cad-to-simready/references/validate-usd-minimum/scripts/check_dependencies.py +++ b/skills/omniverse-cad-to-simready/references/validate-usd-minimum/scripts/check_dependencies.py @@ -14,6 +14,8 @@ from script_utils import check_result as _check, emit_json_report +from preflight_manifest import preflight_required, preflight_status_check + SKILL = "validate-usd-minimum" @@ -23,6 +25,15 @@ def _write_report(payload: dict[str, Any], report_path: Path | None) -> None: def check_dependencies() -> dict[str, Any]: + if preflight_required(): + preflight_check = preflight_status_check(SKILL, "openusd_python") + if not preflight_check["passed"]: + return { + "skill": SKILL, + "passed": False, + "checks": [preflight_check], + "errors": [preflight_check["message"]], + } checks = [ _check("python_available", True, f"Python executable: {sys.executable}"), ] diff --git a/skills/omniverse-cad-to-simready/references/validate-usd-minimum/scripts/run.py b/skills/omniverse-cad-to-simready/references/validate-usd-minimum/scripts/run.py index 1f92e02e..6c5a1e7d 100644 --- a/skills/omniverse-cad-to-simready/references/validate-usd-minimum/scripts/run.py +++ b/skills/omniverse-cad-to-simready/references/validate-usd-minimum/scripts/run.py @@ -15,6 +15,8 @@ from script_utils import usd_bounds_metadata +from preflight_manifest import preflight_required, preflight_status_check + SKILL = "validate-usd-minimum" TOOL = "pxr.Usd" @@ -129,6 +131,12 @@ def validate_minimum_usd(asset_path: Path, next_step: str = DEFAULT_NEXT_STEP) - errors: list[str] = [] metadata = _base_metadata() + if preflight_required(): + preflight_check = preflight_status_check(SKILL, "openusd_python") + if not preflight_check["passed"]: + errors.append(preflight_check["message"]) + return _report(asset_path, False, checks, metadata, warnings, errors, next_step) + exists = asset_path.exists() checks.append(_check("asset_exists", exists, "Asset path exists" if exists else "Asset path does not exist")) if not exists: diff --git a/skills/omniverse-cad-to-simready/references/workflow.md b/skills/omniverse-cad-to-simready/references/workflow.md index 495974ee..8b42b8bb 100644 --- a/skills/omniverse-cad-to-simready/references/workflow.md +++ b/skills/omniverse-cad-to-simready/references/workflow.md @@ -23,6 +23,13 @@ Collect these inputs before running: If a required sidecar path is missing, stop and report the blocked dependency. Do not move or rewrite source assets unless the user explicitly asks. +## Minimum Viable Scope + +Conversion-only is a valid workflow request. When the user asks only to convert +or smoke-test source asset conversion, set `property_assignment_intent=skip`, +do not deploy Content Agents, run `convert-to-usd`, then run +`validate-usd-minimum` on the generated USD if conversion succeeds. + ## Source Routing | Input | Conversion route | @@ -34,11 +41,14 @@ Do not move or rewrite source assets unless the user explicitly asks. | Existing `.usd`, `.usda`, `.usdc`, or `.usdz` | Skip conversion and validate directly | NVIDIA-backed source formats route through the `usd-convert-cad` reference and -must delegate to upstream `usd-convert-cad` only, including the suffixes listed -by upstream `src/usd_convert_cad/formats.py`. If the upstream checkout, setup, -Python 3.12 runtime, `omniverse-kit`, required converter extension, platform -support, licensing, or conversion support is unavailable, mark conversion -blocked rather than substituting another converter. +must delegate to the standalone `usd-convert-cad` wheel only, including the +suffixes listed in the upstream Supported Formats table in the `usd-convert-cad` +SKILL.md (`skills/omniverse-cad-to-usd/SKILL.md`). The `usd-convert-cad` +reference probe parses that table live from a checkout of the repo (with a +built-in snapshot as an offline fallback), so upstream format changes are picked +up automatically. If the wheel is not installed or importable in the resolved +Python 3.12 interpreter, or conversion or licensing support is unavailable, mark +conversion blocked rather than substituting another converter. ## Workflow @@ -214,6 +224,24 @@ The JSON report should include: | `rerun_reasons` | Validation findings, transient service errors, or blocked diagnostics that require later retry/remediation. | | `steps` | Ordered conversion, assignment, conformance, validation, and packaging step results. | +## Consolidated Report Contract + +Emit a consolidated workflow report in Markdown, and include JSON when the +workflow writes structured artifacts. The report must include: + +- Overall status: `passed`, `blocked`, `failed`, or `needs_rerun`. +- Request summary: source asset path, detected source format, output root, + selected SimReady profile/version, and property assignment intent. +- Ordered stage results: stage reference, input artifact, output USD or USDZ + path, report path, status, blocker reason, and rerun reason when applicable. +- Content Agents readiness and property assignment results with service URLs, + tokens, and credentials redacted. +- Conformance and validation findings grouped by gate, requirement ID, selected + FET repair reference, repair-loop attempt, and final disposition. +- Final artifacts: final reported USD path, render preview path when requested, + package root and package validation report when packaging ran, Markdown report + path, JSON report path when present, and recommended next work. + ## Human Approval Points Ask for explicit approval before destructive or ambiguous operations such as diff --git a/skills/omniverse-cad-to-simready/shared/preflight_manifest.py b/skills/omniverse-cad-to-simready/shared/preflight_manifest.py index 6a312ec7..d0aabd7b 100644 --- a/skills/omniverse-cad-to-simready/shared/preflight_manifest.py +++ b/skills/omniverse-cad-to-simready/shared/preflight_manifest.py @@ -7,23 +7,27 @@ import json import os from pathlib import Path +import shutil from typing import Any +from urllib.request import Request, urlopen PREFLIGHT_MANIFEST_ENV = "PHYSICAL_AI_PREFLIGHT_MANIFEST" PREFLIGHT_REQUIRED_ENV = "PHYSICAL_AI_REQUIRE_PREFLIGHT" DEFAULT_MANIFEST_NAME = "cad-to-simready-preflight.json" +SERVICE_HEALTH_CHECK_TIMEOUT_SECONDS = 5.0 +_ENTRY_PATH_KEYS = ("root", "path", "venv") -def skill_hub_home() -> Path: - return Path(os.environ.get("PHYSICAL_AI_SKILL_HUB_HOME", "~/.physical-ai-skill-hub")).expanduser() +def cad_to_simready_home() -> Path: + return Path(os.environ.get("OMNIVERSE_CAD_TO_SIMREADY_HOME", "~/.omniverse-cad-to-simready")).expanduser() def default_manifest_path() -> Path: - state_root = os.environ.get("PHYSICAL_AI_SKILL_HUB_STATE") + state_root = os.environ.get("OMNIVERSE_CAD_TO_SIMREADY_STATE") if state_root: return Path(state_root).expanduser() / DEFAULT_MANIFEST_NAME - return skill_hub_home() / "state" / DEFAULT_MANIFEST_NAME + return cad_to_simready_home() / "state" / DEFAULT_MANIFEST_NAME def configured_manifest_path() -> Path: @@ -57,6 +61,78 @@ def preflight_block_message(target: str, path: Path | None = None) -> str: ) +def _is_remote_or_invocation_endpoint(base_url: str) -> bool: + lowered = base_url.lower() + return lowered.startswith("https://") or "nvcf" in lowered or "invocation" in lowered + + +def _service_health_urls(base_url: str) -> list[str]: + clean = base_url.rstrip("/") + return [f"{clean}/health", f"{clean}/v2/health/ready"] + + +def _service_url_is_healthy(base_url: str, timeout: float = SERVICE_HEALTH_CHECK_TIMEOUT_SECONDS) -> bool: + """Lightweight liveness probe for a manifest-recorded service base URL. + + Mirrors the health-check convention the preflight bootstrap itself uses + (`GET {base_url}/health` or `/v2/health/ready`, HTTP 2xx = healthy) so a + consumer does not keep trusting a service that was healthy when preflight + ran but has since stopped. Remote/invocation endpoints are not probed + here, consistent with how preflight itself treats them as pre-authenticated + and not reachable with a generic unauthenticated GET. + """ + if _is_remote_or_invocation_endpoint(base_url): + return True + for url in _service_health_urls(base_url): + try: + with urlopen(Request(url, method="GET"), timeout=timeout) as response: + if 200 <= response.status < 300: + return True + except Exception: + continue + return False + + +def _path_is_present(value: Any) -> bool: + if not value: + return False + return Path(str(value)).expanduser().exists() + + +def _executable_is_present(value: Any) -> bool: + if not value: + return False + text = str(value) + if _path_is_present(text): + return True + has_separator = os.sep in text or (os.altsep is not None and os.altsep in text) + return not has_separator and shutil.which(text) is not None + + +def _entry_freshness_error(entry: dict[str, Any]) -> str | None: + """Return a reason if a recorded `ready`/`present` entry no longer reflects reality. + + A preflight manifest is a point-in-time snapshot. If the checkout, venv, + executable, or service it recorded as `ready` has since disappeared or + stopped responding, callers must not keep trusting the cached status. + Returns None when the entry still checks out. + """ + base_url = entry.get("base_url") + if base_url: + base_url = str(base_url) + if not _service_url_is_healthy(base_url): + return f"recorded service at {base_url} is no longer reachable" + return None + for key in _ENTRY_PATH_KEYS: + value = entry.get(key) + if value and not _path_is_present(value): + return f"recorded {key} no longer exists: {value}" + executable = entry.get("executable") + if executable and not _executable_is_present(executable): + return f"recorded executable no longer exists: {executable}" + return None + + def runtime_entry(manifest: dict[str, Any] | None, name: str) -> dict[str, Any] | None: if not manifest: return None @@ -101,7 +177,10 @@ def manifest_path_value(entry: dict[str, Any] | None, key: str) -> Path | None: if not entry: return None value = entry.get(key) - return Path(str(value)).expanduser().resolve() if value else None + if not value: + return None + path = Path(str(value)).expanduser().resolve() + return path if path.exists() else None def ready_path_from_runtime(manifest: dict[str, Any] | None, runtime_name: str, key: str = "root") -> Path | None: @@ -123,13 +202,17 @@ def ready_executable_from_runtime(manifest: dict[str, Any] | None, runtime_name: if not entry or entry.get("status") != "ready": return None executable = entry.get("executable") - return str(executable) if executable else None + if not executable or not _executable_is_present(executable): + return None + return str(executable) def ready_service_url(manifest: dict[str, Any] | None, service_name: str) -> str | None: entry = service_entry(manifest, service_name) if entry and entry.get("status") == "ready" and entry.get("base_url"): - return str(entry["base_url"]).rstrip("/") + base_url = str(entry["base_url"]).rstrip("/") + if _service_url_is_healthy(base_url): + return base_url env_name_by_service = { "material": "CONTENT_AGENTS_MATERIAL_AGENT_BASE_URL", "physics": "CONTENT_AGENTS_PHYSICS_AGENT_BASE_URL", @@ -138,7 +221,13 @@ def ready_service_url(manifest: dict[str, Any] | None, service_name: str) -> str } env_name = env_name_by_service.get(service_name) value = manifest_env_value(manifest, env_name) if env_name else None - return value.rstrip("/") if value else None + if not value: + return None + # The manifest `env` block mirrors the same base URLs preflight recorded under + # `services`, so it needs the same liveness probe; otherwise a stopped service + # is still handed back through this fallback. + base_url = value.rstrip("/") + return base_url if _service_url_is_healthy(base_url) else None def preflight_status_check(target: str, component: str) -> dict[str, Any]: @@ -152,9 +241,16 @@ def preflight_status_check(target: str, component: str) -> dict[str, Any]: } entry = runtime_entry(manifest, component) or service_entry(manifest, component) or upstream_entry(manifest, component) passed = bool(entry and entry.get("status") in {"ready", "present"}) + staleness_reason: str | None = None + if passed: + staleness_reason = _entry_freshness_error(entry) + if staleness_reason: + passed = False detail = f"Preflight manifest: {path}" if entry: detail = f"{detail}; {component} status: {entry.get('status')}" + if staleness_reason: + detail = f"{detail}; {staleness_reason}" return { "name": f"preflight_{target}_ready", "passed": passed, diff --git a/skills/omniverse-cad-to-simready/shared/script_utils.py b/skills/omniverse-cad-to-simready/shared/script_utils.py index 55e58c88..2e0c86b6 100644 --- a/skills/omniverse-cad-to-simready/shared/script_utils.py +++ b/skills/omniverse-cad-to-simready/shared/script_utils.py @@ -13,6 +13,8 @@ import tempfile from typing import Any +from preflight_manifest import preflight_required, preflight_status_check + USD_LAYER_SUFFIXES = frozenset({".usd", ".usda", ".usdc", ".usdz"}) @@ -53,6 +55,33 @@ def tail_text(text: str, limit: int = 4000) -> str: return "..." + text[-limit:] +def inspect_png(path: Path) -> dict[str, Any]: + try: + from PIL import Image, ImageStat + except Exception as exc: + return {"available": False, "warning": f"Pillow is unavailable: {exc}"} + + try: + image = Image.open(path).convert("RGB") + except Exception as exc: + return {"available": False, "warning": f"Could not inspect PNG pixels: {exc}"} + small = image.resize((min(64, image.width), min(64, image.height))) + pixels = small.get_flattened_data() if hasattr(small, "get_flattened_data") else small.getdata() + unique = len(set(pixels)) + extrema = image.getextrema() + uniform = unique <= 1 or all(low == high for low, high in extrema) + all_black = all(high == 0 for _, high in extrema) + return { + "available": True, + "size": [image.width, image.height], + "extrema": [[int(low), int(high)] for low, high in extrema], + "unique_colors_after_resize": unique, + "channel_mean": [float(v) for v in ImageStat.Stat(image).mean], + "uniform": bool(uniform), + "all_black": bool(all_black), + } + + def check_result( name: str, passed: bool, @@ -236,6 +265,22 @@ def run_asset_validator_category( ) -> dict[str, Any]: asset_path = asset_path.resolve() command = [validator_tool, "--category", category] + if preflight_required(): + preflight_check = preflight_status_check(validator_skill, "asset_validator") + if not preflight_check["passed"]: + return asset_validation_report( + asset_path=asset_path, + validator_skill=validator_skill, + validator_tool=validator_tool, + category=category, + command=command, + issues=[], + warnings=[], + errors=[preflight_check["message"]], + status="BLOCKED", + next_step=next_step, + severities=severities, + ) command_base, fallback_warnings = _resolve_validator_command(validator_tool, module_tool) if command_base is None: return asset_validation_report( diff --git a/skills/omniverse-cad-to-simready/shared/simready_package.py b/skills/omniverse-cad-to-simready/shared/simready_package.py index a4bf633f..7b16cc74 100644 --- a/skills/omniverse-cad-to-simready/shared/simready_package.py +++ b/skills/omniverse-cad-to-simready/shared/simready_package.py @@ -17,6 +17,8 @@ from script_utils import check_result_with_code as _check, emit_json_report +from preflight_manifest import preflight_required, preflight_status_check + PACKAGING_DEFINITION_FILENAME = "com.nvidia.simready.packaging.json" METADATA_FOLDER = ".metadata" @@ -403,6 +405,13 @@ def _run_wrapp_sample(args: argparse.Namespace) -> dict[str, Any]: def create_package(args: argparse.Namespace) -> dict[str, Any]: + if preflight_required(): + preflight_check = preflight_status_check("nv-core-package-sample", "openusd_python") + if not preflight_check["passed"]: + report = _base_report(args.source.resolve(), "nv-core-package-sample", "package", args.backend, "Package") + report["errors"] = [preflight_check["message"]] + return _finalize(report) + if args.backend == "wrapp": return _run_wrapp_sample(args) @@ -595,6 +604,11 @@ def validate_package(package_definition: Path, *, profile: str) -> dict[str, Any report = _base_report(package_root, "nv-core-package-sample-validation", "validate", "local", profile) report["package_definition_path"] = str(package_definition) checks = report["checks"] + if preflight_required(): + preflight_check = preflight_status_check("nv-core-package-sample-validation", "openusd_python") + if not preflight_check["passed"]: + report["errors"] = [preflight_check["message"]] + return _finalize(report) exists = package_definition.is_file() checks.append(_check("package_definition_exists", exists, "Package definition exists" if exists else "Package definition does not exist", code="PKG.DEF.001")) if not exists: diff --git a/skills/omniverse-cad-to-simready/shared/simready_package_check_dependencies.py b/skills/omniverse-cad-to-simready/shared/simready_package_check_dependencies.py index d2393c0b..1ed4b8d0 100644 --- a/skills/omniverse-cad-to-simready/shared/simready_package_check_dependencies.py +++ b/skills/omniverse-cad-to-simready/shared/simready_package_check_dependencies.py @@ -12,6 +12,8 @@ from script_utils import check_result as _check, emit_json_report +from preflight_manifest import preflight_required, preflight_status_check + def _skill_name() -> str: return Path(sys.argv[0]).resolve().parents[1].name @@ -22,6 +24,16 @@ def _write_report(payload: dict[str, Any], report_path: Path | None) -> None: def check_dependencies() -> dict[str, Any]: + skill = _skill_name() + if preflight_required(): + preflight_check = preflight_status_check(skill, "openusd_python") + if not preflight_check["passed"]: + return { + "skill": skill, + "passed": False, + "checks": [preflight_check], + "errors": [preflight_check["message"]], + } checks = [ _check("python_available", True, f"Python executable: {sys.executable}", "info"), ] diff --git a/skills/omniverse-cad-to-simready/shared/upstream_versions.py b/skills/omniverse-cad-to-simready/shared/upstream_versions.py new file mode 100644 index 00000000..d331e953 --- /dev/null +++ b/skills/omniverse-cad-to-simready/shared/upstream_versions.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Load the CAD-to-SimReady tested upstream lock manifest.""" + +from __future__ import annotations + +from functools import lru_cache +import json +from pathlib import Path +from typing import Any, Iterable + + +UPSTREAM_VERSION_MANIFEST_PATH = Path(__file__).resolve().parents[1] / "upstream-versions.lock.json" + + +@lru_cache(maxsize=1) +def load_upstream_version_manifest() -> dict[str, Any]: + payload = json.loads(UPSTREAM_VERSION_MANIFEST_PATH.read_text(encoding="utf-8")) + if payload.get("schema_version") != "1.0": + raise ValueError(f"unsupported upstream version manifest: {UPSTREAM_VERSION_MANIFEST_PATH}") + return payload + + +def git_upstreams() -> dict[str, dict[str, str]]: + return load_upstream_version_manifest()["git_upstreams"] + + +def package_version(name: str) -> str: + return str(load_upstream_version_manifest()["python_packages"][name]) + + +def package_requirement(name: str) -> str: + return f"{name}=={package_version(name)}" + + +def runtime_package_names(runtime: str, *, no_deps: bool = False) -> tuple[str, ...]: + key = "no_deps" if no_deps else "packages" + return tuple(load_upstream_version_manifest()["runtime_sets"][runtime][key]) + + +def runtime_requirements(runtime: str, *, no_deps: bool = False) -> tuple[str, ...]: + return tuple(package_requirement(name) for name in runtime_package_names(runtime, no_deps=no_deps)) + + +def expected_versions(names: Iterable[str]) -> dict[str, str]: + return {name: package_version(name) for name in names} + + +def version_probe_code(names: Iterable[str]) -> str: + distributions = tuple(names) + return ( + "import importlib.metadata as m, json; " + f"names={distributions!r}; " + "norm=lambda value: value.lower().replace('_', '-').replace('.', '-'); " + "installed={norm(d.metadata['Name']): d.version for d in m.distributions() if d.metadata['Name']}; " + "print(json.dumps({name: installed.get(norm(name)) for name in names}, sort_keys=True))" + ) + + +def version_mismatches(expected: dict[str, str], installed: dict[str, str | None]) -> list[str]: + return [ + f"requires {name}=={version}, found {installed.get(name) or 'not installed'}" + for name, version in expected.items() + if installed.get(name) != version + ] diff --git a/skills/omniverse-cad-to-simready/shared/usd_convert_cad_diagnostics.py b/skills/omniverse-cad-to-simready/shared/usd_convert_cad_diagnostics.py deleted file mode 100644 index c9f22e15..00000000 --- a/skills/omniverse-cad-to-simready/shared/usd_convert_cad_diagnostics.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import re -from typing import Any -from urllib.parse import urlparse - - -DOWNLOAD_RE = re.compile(r"Failed to download:\s*['\"](?Phttps?://[^'\"]+)['\"]") -EXTENSION_RE = re.compile( - r"(?:Pulling extension:\s*`|Failed to pull extension:\s*['\"])(?P[^`'\"]+)" -) -CACHE_RE = re.compile(r"Failed to pull extension:\s*['\"][^'\"]+['\"]\s+in\s+['\"](?P[^'\"]+)['\"]") -RESULT_RE = re.compile(r"\b(?PResult\.[A-Z0-9_]+)\b") -KIT_LOG_RE = re.compile(r"Logging to file:\s*(?P\S.+)") - - -def summarize_usd_convert_cad_validation_failure(output: str, exit_code: int | None) -> dict[str, Any] | None: - """Return a structured diagnostic for known upstream Kit registry failures.""" - text = output or "" - if not text: - return None - - download_url = _first_match(DOWNLOAD_RE, text, "url") - extension = _last_match(EXTENSION_RE, text, "extension") - result = _first_match(RESULT_RE, text, "result") - has_access_denied = result == "Result.ERROR_ACCESS_DENIED" or "Result.ERROR_ACCESS_DENIED" in text - has_kit_download = bool(download_url and (extension or "Failed to pull extension" in text)) - if not has_access_denied and not has_kit_download: - return None - - url_host = urlparse(download_url).netloc if download_url else "" - kind = "kit_registry_access_denied" if has_access_denied else "kit_registry_download_failure" - action = "access denied" if kind == "kit_registry_access_denied" else "download failed" - target = f" while fetching {extension}" if extension else "" - source = f" from {url_host}" if url_host else "" - summary = f"Kit extension registry {action}{target}{source}." - recovery_hint = ( - "Verify the Horde host can reach the Kit extension registry/CDN with the required network, " - "proxy, or credentials, or pre-populate and reuse the upstream usd-convert-cad Kit extension " - "cache; then rerun `OMNI_KIT_ACCEPT_EULA=yes python validate.py` in the upstream checkout." - ) - - diagnostic: dict[str, Any] = { - "kind": kind, - "summary": summary, - "recovery_hint": recovery_hint, - } - if exit_code is not None: - diagnostic["exit_code"] = exit_code - if result: - diagnostic["result"] = result - if extension: - diagnostic["extension"] = extension - if download_url: - diagnostic["download_url"] = download_url - if url_host: - diagnostic["url_host"] = url_host - cache_path = _last_match(CACHE_RE, text, "cache") - if cache_path: - diagnostic["cache_path"] = cache_path - kit_log_path = _last_match(KIT_LOG_RE, text, "path") - if kit_log_path: - diagnostic["kit_log_path"] = kit_log_path.strip() - return diagnostic - - -def _first_match(pattern: re.Pattern[str], text: str, group: str) -> str: - match = pattern.search(text) - return match.group(group).strip() if match else "" - - -def _last_match(pattern: re.Pattern[str], text: str, group: str) -> str: - matches = list(pattern.finditer(text)) - return matches[-1].group(group).strip() if matches else "" diff --git a/skills/omniverse-cad-to-simready/skill-card.md b/skills/omniverse-cad-to-simready/skill-card.md index 03c8b299..b77aec81 100644 --- a/skills/omniverse-cad-to-simready/skill-card.md +++ b/skills/omniverse-cad-to-simready/skill-card.md @@ -1,5 +1,5 @@ ## Description:
-Coordinate the end-to-end CAD/source-asset to SimReady workflow including conversion, material/physics assignment, SimReady conformance, validation, and optional package creation.
+Coordinate the end-to-end CAD/source-asset to SimReady workflow, including conversion, material/physics assignment, SimReady conformance, validation, and optional package creation.
This skill is ready for commercial/non-commercial use.
@@ -7,13 +7,19 @@ This skill is ready for commercial/non-commercial use.
NVIDIA
### License/Terms of Use:
-Apache 2.0
+Apache-2.0
## Use Case:
-Developers and engineers converting CAD and source assets to simulation-ready OpenUSD with automated material/physics property assignment, SimReady profile conformance, validation, and packaging.
+Developers and engineers converting CAD or source assets into simulation-ready USD packages with automated property assignment, conformance validation, and optional packaging.
### Deployment Geography for Use:
Global
+## Requirements / Dependencies:
+**Requires API Key or External Credential:** [Yes]
+**Credential Type(s):** [API key]
+ +Do not include secrets in prompts/logs/output; use least-privilege credentials; rotate keys as appropriate.
+ ## Known Risks and Mitigations:
Risk: Review before execution as proposals could introduce incorrect or misleading guidance into skills.
Mitigation: Review and scan skill before deployment.
@@ -26,28 +32,61 @@ Mitigation: Review and scan skill before deployment.
- [Content Agents](references/content-agents/README.md)
- [SimReady Conform Profile](references/simready-conform-profile/README.md)
- [SimReady Validate](references/simready-validate/README.md)
-- [Assemble Package Source](references/assemble-package-source/README.md)
+- [Asset Validator](references/omni-asset-validate/README.md)
- [OVRTX Render Service](references/ovrtx-render-service/README.md)
+- [Troubleshooting](references/troubleshooting.md)
## Skill Output:
-**Output Type(s):** [Files, Shell commands, Analysis]
-**Output Format:** [Markdown with JSON structured artifacts]
+**Output Type(s):** [Analysis, Shell commands, Files]
+**Output Format:** [Markdown with inline JSON structured artifacts]
**Output Parameters:** [1D]
-**Other Properties Related to Output:** [None]
+**Other Properties Related to Output:** [Consolidated workflow report with stage statuses, validation findings, and output USD paths]
+ +## Evaluation Agents Used:
+- Claude Code (`aws/anthropic/bedrock-claude-opus-4-8`)
+- Codex (`openai/openai/gpt-5.5`)
+ + + +## Evaluation Tasks:
+Evaluated against 8 tasks (7 positive, 1 negative) in isolated k8s-sandbox pods with 1 attempt per task.
## Evaluation Metrics Used:
Reported benchmark dimensions:
-- Security: Checks whether skill-assisted execution avoids unsafe behavior such as secret leakage, destructive commands, or unauthorized access.
-- Correctness: Checks whether the agent follows the expected workflow and produces the correct final output.
-- Discoverability: Checks whether the agent loads the skill when relevant and avoids using it when irrelevant.
-- Effectiveness: Checks whether the agent performs measurably better with the skill than without it.
-- Efficiency: Checks whether the agent uses fewer tokens and avoids redundant work.
+- Security: Whether the skill avoids unsafe operations, secret leakage, and unauthorized access.
+- Correctness: Whether the final answer is correct against the reference answer.
+- Discoverability: Whether the expected skill is found and executed when needed.
+- Effectiveness: Whether the skill helps the agent complete the user's goal and expected workflow.
+- Efficiency: Whether the skill avoids wasted tool or skill usage.
+ +Underlying evaluation signals used in this run:
+- `security`: Unsafe operations, secret leakage, and unauthorized access.
+- `skill_execution`: Whether the expected skill was found and executed.
+- `skill_efficiency`: Routing quality, workspace-aware skill reads, and productive tool use.
+- `accuracy`: Final-answer correctness against the reference answer.
+- `goal_accuracy`: Whether the user's goal was achieved.
+- `behavior_check`: Whether the expected workflow behavior was followed.
+ + +## Evaluation Results:
+| Measure | Codex (Baseline → Skill Uplift) | +|---|---:| +| Overall | 48% → 82% (+33 points) | +| Security | 50% → 81% (+31 points) | +| Correctness | 57% → 88% (+30 points) | +| Discoverability | 48% → 81% (+34 points) | +| Effectiveness | 37% → 75% (+38 points) | +| Efficiency | 49% → 83% (+34 points) | +## Testing Completed:
+**[x] Agent Red-Teaming**
+**[ ] Network Security**
+**[ ] Product Security**
## Skill Version(s):
-0.1.0 (source: frontmatter, pyproject.toml)
+0.2.0 (source: frontmatter, changelog)
## Ethical Considerations:
NVIDIA believes Trustworthy AI is a shared responsibility and we have established policies and practices to enable development for a wide array of AI applications. When downloaded or used in accordance with our terms of service, developers should work with their internal team to ensure this skill meets requirements for the relevant industry and use case and addresses unforeseen product misuse.
diff --git a/skills/omniverse-cad-to-simready/skill.oms.sig b/skills/omniverse-cad-to-simready/skill.oms.sig index 37f6c942..3c7924e0 100644 --- a/skills/omniverse-cad-to-simready/skill.oms.sig +++ b/skills/omniverse-cad-to-simready/skill.oms.sig @@ -1 +1 @@ -{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"x509CertificateChain":{"certificates":[{"rawBytes":"MIICgzCCAgmgAwIBAgIUKIyS7SxNteQIiWzK1dWj85E6520wCgYIKoZIzj0EAwMwVTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjEpMCcGA1UEAwwgTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBJQ0EgMDEwHhcNMjYwNDAxMDAwMDAwWhcNMjgwNDIyMTUzMzA5WjBUMQswCQYDVQQGEwJVUzEbMBkGA1UECgwSTlZJRElBIENvcnBvcmF0aW9uMSgwJgYDVQQDDB9OVklESUEgQWdlbnQgU2tpbGxzIFNpZ25pbmcgMDAxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEYoRM9bQl/dGlwSRNi6bTpIJUXH8Nv9GciP6LSflJYYMLCc296kpyuTSsk5ddbAWiDcFX3C/ydX3jwc+qCLYP6uHy9XphyLjOQ27Yb2J6rBLVtRBS1mgGco/Gr7fL6ODco4GaMIGXMB0GA1UdDgQWBBRQ/5ZW3nJ6lmo9SVk7I15o7UGmpTAfBgNVHSMEGDAWgBRPGpILxMBBleJSsBGjrMKsby1CgjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUHMAGGG2h0dHA6Ly9vY3NwLm5kaXMubnZpZGlhLmNvbTAKBggqhkjOPQQDAwNoADBlAjAUygu/GiOCIXrgGr4SmLgeEVDcEitfFUv7ALbvLVGVyMysB3mxmO/uInZfXzWcJZsCMQDxuoxj4ZmO30jhkPIcCxGFCOvnUsnfU3TfGcouYm4M6iRpbKvtVnHPiy4bi6pcKf0="},{"rawBytes":"MIICiDCCAg6gAwIBAgIUZsIuSv9NkpJCNqtYEfCouVv5BzowCgYIKoZIzj0EAwMwUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTAgFw0yNjA0MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowVTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjEpMCcGA1UEAwwgTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBJQ0EgMDEwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASI72cR3ctKGg4VWnB3bNja6g1Z2PnOmFEopkPof+QeIcPk9rT+g9MjJnq51EQXL93a7C2GJ9J985G4o2V85VD7wJ1RaXhluHW2rf3y8bQGeAYaKMr5s/hUgn+M3/9WlWejgaAwgZ0wHQYDVR0OBBYEFE8akgvEwEGV4lKwEaOswqxvLUKCMB8GA1UdIwQYMBaAFItnoAjjfuCEUvzyvWyI2vOGvwPjMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMDcGCCsGAQUFBwEBBCswKTAnBggrBgEFBQcwAYYbaHR0cDovL29jc3AubmRpcy5udmlkaWEuY29tMAoGCCqGSM49BAMDA2gAMGUCMQCeIMMfAbyzPDacw2MxG+Yt1cikrJX/DVxiGfXuHmkkXn6VgSzE79+lkqDErpVO2gYCMCNEColOyvUvkzZGUEI1hQ3PfMgi3FIo9tHoBKMw4/wGBLFpu/0ubtmbBXM6/UMOEw=="},{"rawBytes":"MIICRTCCAcygAwIBAgIUeJdY3rV86EdvFmG7L8LJBsyQFYkwCgYIKoZIzj0EAwMwUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTAgFw0yNjA0MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABAYpiXCDjJ9NT2eSDhyHJVSw1Tbze18cGG2F/578oWvHxg23eQAhNRYdq88i1iOshZSO6C29doKui5Xpmo/7Ctw9Sx4PP2RzOmIuOLCuTdNtKcTRwi4GEsd5BAFvWj42M6NjMGEwHQYDVR0OBBYEFItnoAjjfuCEUvzyvWyI2vOGvwPjMB8GA1UdIwQYMBaAFItnoAjjfuCEUvzyvWyI2vOGvwPjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cAMGQCMCwtAjWLaNwgGWNCgdyNoTyvNhqWRECRJV2r3+7w8g0PL6NHLOsbkgE09BH95h8XlgIwTaQmbbUh2ChAJ5TA1wRiVDnCcvbzHlZl2jM2FcwQQZlk19LOAbyGMRixbu2Ww/rj"}]},"tlogEntries":[]},"dsseEnvelope":{"payload":"ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YxIiwKICAic3ViamVjdCI6IFsKICAgIHsKICAgICAgIm5hbWUiOiAib21uaXZlcnNlLWNhZC10by1zaW1yZWFkeSIsCiAgICAgICJkaWdlc3QiOiB7CiAgICAgICAgInNoYTI1NiI6ICI4YWI3MDNhZGY4Yjk5MmM5NDBkZmVlMmQxNjgwODMxZWVhYTY5NGNhYzE1YTQ4ZDAwMzUxNTZjZWI3NDg5ZTBiIgogICAgICB9CiAgICB9CiAgXSwKICAicHJlZGljYXRlVHlwZSI6ICJodHRwczovL21vZGVsX3NpZ25pbmcvc2lnbmF0dXJlL3YxLjAiLAogICJwcmVkaWNhdGUiOiB7CiAgICAic2VyaWFsaXphdGlvbiI6IHsKICAgICAgImlnbm9yZV9wYXRocyI6IFsKICAgICAgICAiLmdpdCIsCiAgICAgICAgIi5naXRpZ25vcmUiLAogICAgICAgICIuZ2l0aHViIiwKICAgICAgICAiLmdpdGF0dHJpYnV0ZXMiCiAgICAgIF0sCiAgICAgICJoYXNoX3R5cGUiOiAic2hhMjU2IiwKICAgICAgImFsbG93X3N5bWxpbmtzIjogZmFsc2UsCiAgICAgICJtZXRob2QiOiAiZmlsZXMiCiAgICB9LAogICAgInJlc291cmNlcyI6IFsKICAgICAgewogICAgICAgICJuYW1lIjogIkJFTkNITUFSSy5tZCIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiMjE3ZjdjYzI2MDY5MTY2OGIxMzY0ODJjNmFhMDMzNzE0Yjk4ODRiNDU1NTIxZjdlMGRjNjhiMTM2MjViZDE5OCIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogIlNLSUxMLm1kIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI4ZmYzNGY0Nzg2MTA2MTNiZDM3YWExOWNlMGMzYjBmZGMyMzUyMjI2Y2E1NjFiODMxNzk5ZmNlNGRiMDY5NTMxIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAiYWdlbnRzL29wZW5haS55YW1sIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICIxMGM4Yjk3NTYwNmRmMTg2ZTg3OTZmZjZlMThmNzZkZDkxMDJiYjhkNDUyNmE1ZWUxMGE0ZDViMThkZjdkZmExIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAiZXZhbHMvZXZhbHMuanNvbiIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiZjM3MmU4OWRkZDI1NTVkNjU0NzgxZTM4MTZkMzZhN2VjZThjY2FkYWMxZWQzNmY2MTI1MGZjMGUzOWNjNTk3ZSIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogImV2YWxzL2ZpbGVzL21pbmltYWxfbWVzaC5zdGwiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogIjU1YTMyMjgxOTUwMmQyMDFkMjhjYTlhMmRjM2ZlZGFiOGIzYTUxZTExNGJlY2JiZDk3M2JiNThmNWQ2Mzk2YzEiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2Fzc2VtYmxlLXBhY2thZ2Utc291cmNlL1JFQURNRS5tZCIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiZTc0MDdkODkyZTgwYWU5N2YzZmY1YTRkMzRlZjBhODViMmUxZDc2YTU3NDcyZDFlNzE3M2NiMjZkZDFhYWU2MSIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvYXNzZW1ibGUtcGFja2FnZS1zb3VyY2Uvc2NyaXB0cy9jaGVja19kZXBlbmRlbmNpZXMucHkiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImVhYzM4MDMzNWY0NjNjMmIyNjU0OGVmZGRkZDMzZGY0OGViMGFlNzZiNjE3ZmUwZTU3MGE3YWYwZWU4ZGUwODIiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2Fzc2VtYmxlLXBhY2thZ2Utc291cmNlL3NjcmlwdHMvcmVwb3J0X3NjaGVtYS5qc29uIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICJiMzZiNDk0NTU0YjlmMjBlYzJiNDE1ODkxNmFlNTc2NWVjNTYyOWQ2Yjc5OGJmZjgwMzA0ZWY1OGFmNjg3NDY2IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9hc3NlbWJsZS1wYWNrYWdlLXNvdXJjZS9zY3JpcHRzL3J1bi5weSIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiOTAzMTk4YTcyMTU3ZjNjOTU2OTA2Y2FkMjA1YmNkNDBkMzdiYjUwMWRhZWVhYmM4MjgyYzZlNDU1ZmQyNWUyYyIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29tbWFuZHMubWQiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImE1NWM1MWRkNDcxNDE0NTVhN2QxNjIzMmEyNDI2ZGYzNmNiODc1NTc4ZDgyZmE1NWM2NzJlY2VhZmU2ZTg2NWQiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnRlbnQtYWdlbnRzL1JFQURNRS5tZCIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiZGYxNGE0NDkyM2VjM2RmZGQ0Mjc2MDQzYzU5MDA0YzYzNDAzMDA2YjFmMDhhMDdlZTA0Yjc1YTBjYzRhMjAyYyIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udGVudC1hZ2VudHMvcmVmZXJlbmNlcy9tYXRlcmlhbC1hZ2VudC1jbGllbnQvUkVBRE1FLm1kIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICJiN2E1ZmZmZDNhODYxYWZhZmJhNDI0NzYzY2VlMWVkZDRjZDA2NThmMmI4ODFjZjk5NGRjYTk4Y2UyMWQxZDVmIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb250ZW50LWFnZW50cy9yZWZlcmVuY2VzL21hdGVyaWFsLWFnZW50LWNsaWVudC9zY3JpcHRzL2NoZWNrX2RlcGVuZGVuY2llcy5weSIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiYjAwODczM2VjNjVmZGM4YWQ3YTlkOWRkOGRkM2YxNmRmMjE2M2UzMDliYmMyYmI3YjIxZWFkOGQ0MmFiMzIxNiIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udGVudC1hZ2VudHMvcmVmZXJlbmNlcy9tYXRlcmlhbC1hZ2VudC1jbGllbnQvc2NyaXB0cy9yZXBvcnRfc2NoZW1hLmpzb24iLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImNkYzg5MzE2MWI2OTAxMmNkMjE3ZmFhYWIyMzc5MDFjZDA3NjM5ODJmZTYyZGNiZTMzNzEyYmE0OTZmYWVlNGYiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnRlbnQtYWdlbnRzL3JlZmVyZW5jZXMvbWF0ZXJpYWwtYWdlbnQtY2xpZW50L3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICJjZDk5MzQ5NTgxN2NiNDhlY2VjZmQxOTNiMDBiOTFiZjQ1ZjdmYTNmZWVjZWI2MzlhZTI4Yzg4MTAwYzgxMjg1IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb250ZW50LWFnZW50cy9yZWZlcmVuY2VzL3BoeXNpY3MtYWdlbnQtY2xpZW50L1JFQURNRS5tZCIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiNTYzYmY0Y2MzZWE3OTEwYmM2ODNhMTU5ZTAyZmY2MzM4MDFiODM0ZWJiMmQ5NjM4ZGFhZjlhMTBkZDEwZTJiYSIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udGVudC1hZ2VudHMvcmVmZXJlbmNlcy9waHlzaWNzLWFnZW50LWNsaWVudC9zY3JpcHRzL2NoZWNrX2RlcGVuZGVuY2llcy5weSIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiYjAwODczM2VjNjVmZGM4YWQ3YTlkOWRkOGRkM2YxNmRmMjE2M2UzMDliYmMyYmI3YjIxZWFkOGQ0MmFiMzIxNiIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udGVudC1hZ2VudHMvcmVmZXJlbmNlcy9waHlzaWNzLWFnZW50LWNsaWVudC9zY3JpcHRzL3JlcG9ydF9zY2hlbWEuanNvbiIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiY2RjODkzMTYxYjY5MDEyY2QyMTdmYWFhYjIzNzkwMWNkMDc2Mzk4MmZlNjJkY2JlMzM3MTJiYTQ5NmZhZWU0ZiIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udGVudC1hZ2VudHMvcmVmZXJlbmNlcy9waHlzaWNzLWFnZW50LWNsaWVudC9zY3JpcHRzL3J1bi5weSIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiY2Q5OTM0OTU4MTdjYjQ4ZWNlY2ZkMTkzYjAwYjkxYmY0NWY3ZmEzZmVlY2ViNjM5YWUyOGM4ODEwMGM4MTI4NSIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udGVudC1hZ2VudHMvcmVmZXJlbmNlcy90ZXh0dXJlLWFnZW50LWNsaWVudC9SRUFETUUubWQiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImM1ODg2NGFjZWUzNDgzNzI4YzJlMzBmNmRkNWM0YTViYjg0YTgwOTNmYTc1YzVmZTJmMTNmZDIxMDc4MzQ0ZWEiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnRlbnQtYWdlbnRzL3JlZmVyZW5jZXMvdGV4dHVyZS1hZ2VudC1jbGllbnQvc2NyaXB0cy9jaGVja19kZXBlbmRlbmNpZXMucHkiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImIwMDg3MzNlYzY1ZmRjOGFkN2E5ZDlkZDhkZDNmMTZkZjIxNjNlMzA5YmJjMmJiN2IyMWVhZDhkNDJhYjMyMTYiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnRlbnQtYWdlbnRzL3JlZmVyZW5jZXMvdGV4dHVyZS1hZ2VudC1jbGllbnQvc2NyaXB0cy9yZXBvcnRfc2NoZW1hLmpzb24iLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImNkYzg5MzE2MWI2OTAxMmNkMjE3ZmFhYWIyMzc5MDFjZDA3NjM5ODJmZTYyZGNiZTMzNzEyYmE0OTZmYWVlNGYiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnRlbnQtYWdlbnRzL3JlZmVyZW5jZXMvdGV4dHVyZS1hZ2VudC1jbGllbnQvc2NyaXB0cy9ydW4ucHkiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImNkOTkzNDk1ODE3Y2I0OGVjZWNmZDE5M2IwMGI5MWJmNDVmN2ZhM2ZlZWNlYjYzOWFlMjhjODgxMDBjODEyODUiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnRlbnQtYWdlbnRzL3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI4NzJkY2UxZjljZmNkOTQwMGUwYjc0MzhiNTU5MjcwY2Q4OWE5MTYyN2JkOGY1MzRiM2IyMDMxZDM0NmMxMGUxIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb250ZW50LWFnZW50cy9zY3JpcHRzL2NvbnRlbnRfYWdlbnRfY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICJkODBjM2EyMmZlNTAzZDQ4ZGEwN2IyNDA3OTZmMGEyODM4ODAyMTAzMjRhNmQ0NTQxZDY5YmE5ZTRhODMyOTZhIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb250ZW50LWFnZW50cy9zY3JpcHRzL2NvbnRlbnRfYWdlbnRfY2xpZW50LnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICIyMWFhZmMwZTRiODMwZDJiZDgyMjhiMzA3MzY4N2NlNzExMGFjYzI4YjIxZjM2N2FjMDcxYjFlNTc1NWIwMDliIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb250ZW50LWFnZW50cy9zY3JpcHRzL2NvbnRlbnRfYWdlbnRfbWF0ZXJpYWxfY2xlYW51cC5weSIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiNjAzMTg1MmNjZThkNzAzM2I3YzhkN2VhMWIzOTZkNzcyYWI0OWRhYTJiNGYwNDhjMzI4OWQwN2FkYWQ0NmQ4MyIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udGVudC1hZ2VudHMvc2NyaXB0cy9jb250ZW50X2FnZW50X3JlcG9ydF9zY2hlbWEuanNvbiIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiY2RjODkzMTYxYjY5MDEyY2QyMTdmYWFhYjIzNzkwMWNkMDc2Mzk4MmZlNjJkY2JlMzM3MTJiYTQ5NmZhZWU0ZiIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udGVudC1hZ2VudHMvc2NyaXB0cy9yZXBvcnRfc2NoZW1hLmpzb24iLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImI1YTE3ZTIzYzMxMzVlOTk3YmQ3ZGJhOWRiZWY2NDczMDIyY2M1NTllYzA4OTExNmJkNWZkODBhYTJmMGE4ZjgiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnRlbnQtYWdlbnRzL3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICJkNmVkMzkzNDlmYWNlYmNkYzM3ZmRlNGM0MzdiNDZlMjRiN2JkN2Q2ODAzYzhmNzc5Mjk5ZGNjZDA1N2FhYjI5IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9SRUFETUUubWQiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImQ4YjUwNTU2Yzc2NzhjOTY1YzQyMGRlMzQ0YjI3YTk3Njk1MDg3MDY0ZWNjNDYwN2M1NmFhMjhlMTk1OTkxZmUiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnZlcnQtdG8tdXNkL3JlZmVyZW5jZXMvbXVqb2NvLXVzZC1jb252ZXJ0ZXIvUkVBRE1FLm1kIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICJmY2VjMTQ4M2QyOThmMmJjZmRmODY5MDE2NzQ3NmM1NWNmNWFmOTIzNzlmMGMxZmYxYTVkMzIzZmNlNDcxMzg4IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9yZWZlcmVuY2VzL211am9jby11c2QtY29udmVydGVyL3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI2MjcxNmJmMzZlYWFhZTc4MDg1ZjAyN2E3Y2M4ZWMwODNjZmNlZjY1OTg4NzMzZGRkOWFmOWYwNTJhMDM3ZmE2IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9yZWZlcmVuY2VzL211am9jby11c2QtY29udmVydGVyL3NjcmlwdHMvcmVwb3J0X3NjaGVtYS5qc29uIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICJiMmYwMThjNWZlY2JjOWFjZjkyZmFjOTRjMWFmM2RmYTA1YTc5NTJkZWViNTdmMmIwYzg4ZTA3YjZlMWEyZWI2IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9yZWZlcmVuY2VzL211am9jby11c2QtY29udmVydGVyL3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICJiMzJhODA1YTk3ZTg0MTVhYTA5OGU3MTk2NDJjZGQ2MDc2ZDMxNDdlNWJkNDJhMTUzNDg0ZTA1NGNiZWQwNWQ0IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9yZWZlcmVuY2VzL29wZW51c2QtZXhjaGFuZ2Utc2RrLXVzZGV4L1JFQURNRS5tZCIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiNWRlZjg3NGIxMDdkMWRkMjJhYzc0NTA5YzE1ZmVlOWJjZGE3ZGY2MzE3YmQ1YTg4ZGE2MjI1MDQ1ZGMwMjViOCIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udmVydC10by11c2QvcmVmZXJlbmNlcy91cmRmLXVzZC1jb252ZXJ0ZXIvUkVBRE1FLm1kIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICJiZTg3OTc2N2IwYmU4OGY1ZWI4NjAyNjQzMGRkY2M2NDlkZDFlZTk2YTY0YmFmYzVlNzdjNTViZjZiN2NhODU0IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9yZWZlcmVuY2VzL3VyZGYtdXNkLWNvbnZlcnRlci9zY3JpcHRzL2NoZWNrX2RlcGVuZGVuY2llcy5weSIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiNzVjNTAzMDA2YTRiODMzODNmM2QyOTkxN2FkMTcxMTg4MTM3MDk2N2VmNDM1ZmVjODIzZTJjNjMzZjUzNzZiNyIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udmVydC10by11c2QvcmVmZXJlbmNlcy91cmRmLXVzZC1jb252ZXJ0ZXIvc2NyaXB0cy9yZXBvcnRfc2NoZW1hLmpzb24iLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogIjQ3NWJhMzMxYzRmYzhhYzk5MTkyOWY1MmQ4Mjk3M2M2Yjk1ZTAyMjkyNTVjNjk0ZmNmZjY5MzE3MTM5MWI3MmIiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnZlcnQtdG8tdXNkL3JlZmVyZW5jZXMvdXJkZi11c2QtY29udmVydGVyL3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI3NGY3ZThkZTZjMTIxYzY4MjQ3ZGQxNzliMjE5ZjA0NTc5NWRkNGQyNjEyNmQyYTVlN2UzMjZjYmZlMmQ2ZTQxIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9yZWZlcmVuY2VzL3VzZC1jb252ZXJ0LWNhZC9SRUFETUUubWQiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogIjFhNDRlNWRjMTBlNzk0NDgxMzkyMGQzY2RjZjZiNGUzNWNkNDc1ZGZhMGEyOGRkNzEyZDJmYmRkYTExNzA5YTEiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnZlcnQtdG8tdXNkL3JlZmVyZW5jZXMvdXNkLWNvbnZlcnQtY2FkL3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICJkOWU1MjNmN2ViZWQwNTJkMjA1MGM4NTk0NTJmNWFiMTM5ODQ5ZTZmZjBjNjVmMWE4YmI4MjEyNjMyNmIzNDIwIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9yZWZlcmVuY2VzL3VzZC1jb252ZXJ0LWNhZC9zY3JpcHRzL2tpdF9hcHBfdGVtcGxhdGVfY2FkLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI4MzMzNjY0OTEyMmYwNjhhNGI5N2ZhNmJiNzFkOTY4MzdiYjYzYTlmOGRjMGVmODVhMTI5OGMxMjM0MDUzZmY3IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9yZWZlcmVuY2VzL3VzZC1jb252ZXJ0LWNhZC9zY3JpcHRzL3JlcG9ydF9zY2hlbWEuanNvbiIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiNzNiNzI0M2FlOGYyYWFhMjEzM2ZhM2M1MjIyZmVlZWEwYzRiNGY1Y2RmOGVhZDQ1MDM0NzdmNTQ3OGRkOWQ4MSIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udmVydC10by11c2QvcmVmZXJlbmNlcy91c2QtY29udmVydC1jYWQvc2NyaXB0cy9ydW4ucHkiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImJkZjZlNGNkMmU2OGQzNzRmMjdjYTMxYjc4N2E0M2NkN2FmMDBmYzY2MGQ1NmIxZDk3MjQyZWFiOWRmMDRlMzIiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnZlcnQtdG8tdXNkL3JlZmVyZW5jZXMvdXNkLWNvbnZlcnQtZ3NwbGF0L1JFQURNRS5tZCIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiODA3ODZiZGE1YzY1ODU3NWQ3YmEzMjE2ZDE1YzZlZDQzMDcxMTc2MDk5NjA0OGU3ZjE2NjllMzAyYTMyZDYwMiIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udmVydC10by11c2QvcmVmZXJlbmNlcy91c2QtY29udmVydC1nc3BsYXQvc2NyaXB0cy9jaGVja19kZXBlbmRlbmNpZXMucHkiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogIjRhMTM0Njg5MDc5Y2EzZTc3OTE5MDliN2NkNGExM2VmMjM1ZDZjNTlhMjM3OTVjOGM1ZDc1MWVhMTY2NGY5ODUiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnZlcnQtdG8tdXNkL3JlZmVyZW5jZXMvdXNkLWNvbnZlcnQtZ3NwbGF0L3NjcmlwdHMvcmVwb3J0X3NjaGVtYS5qc29uIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICJjNDMzMzYwZmZiOGVjYWY2YWRjYTQwMGQ0ZjdjOGQxYTBkYTkwODlhZDNhNjY1NzNkNDE5ZjJkZTI4NjRhNDhmIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9yZWZlcmVuY2VzL3VzZC1jb252ZXJ0LWdzcGxhdC9zY3JpcHRzL3J1bi5weSIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiYmRlN2RlNTEwYjVkZDExZTJhOTI0Zjc0MmM4MjI1OWY0NTI2ZWU1YzdhODU3ZDIyOTIxMmE1ZmRiYTljYThjNyIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udmVydC10by11c2Qvc2NyaXB0cy9jaGVja19kZXBlbmRlbmNpZXMucHkiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImQ1ZjY1ODkyYTc3MGYzN2QyYjVmMzNjODcwNmIyNWNjYjU0ZmNiMDI2ZGE4YmZmYmI5ZDI5NDU5OTI1MDFiYTciCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnZlcnQtdG8tdXNkL3NjcmlwdHMvcmVwb3J0X3NjaGVtYS5qc29uIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICIyMmVhODQ0NzY5YmJkYjRhNjMxMGQzM2M5YWNhMWRmZTg2ZTNmMzVjOTA0OTBkOWU5Y2Q3ZDkxYWQ2OWEyYTIxIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9zY3JpcHRzL3J1bi5weSIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiZDFiYjIxYjdkMjk4MWRhNjg0M2RlYmNiMWQxMjFiNzRiMjBjYTk4ZDM4ZGZkYzMwMzI4ZDc3N2EyNWU5YmNhMSIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvZGVwbG95LWNvbnRlbnQtYWdlbnRzL1JFQURNRS5tZCIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiN2FjODFkY2QzMGE1ZWNlZjQ3NGMxNTQ5MjBlZjgzMjBjYWQ5N2RhNWI2NGM5N2QxYTM4MzM2MWRmZGVjOGZiZCIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvZGVwbG95LWNvbnRlbnQtYWdlbnRzL3JlZmVyZW5jZXMvZGVwbG95LW1hdGVyaWFsLWFnZW50LWRvY2tlci5tZCIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiMmU0YTMwNzY5NGJiZmFkNzJjNDdhMGM4ODBkZjUwNWQ0NTkzZTcwZDkwODM2MGViYjIyZjU3OTI0YzI2NmJkMiIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvZGVwbG95LWNvbnRlbnQtYWdlbnRzL3JlZmVyZW5jZXMvZGVwbG95LW92cnR4LWRvY2tlci5tZCIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiZWJjOTg0MWU5NzQ4NDQzMGRjODdmMDk2ZDliYmM1NzNhMzBkNGEwNzcwY2NkNTJkZTRjMGFlMDRmYjE2OTM3NSIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvZGVwbG95LWNvbnRlbnQtYWdlbnRzL3JlZmVyZW5jZXMvZGVwbG95LXBoeXNpY3MtYWdlbnQtZG9ja2VyLm1kIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI5NTA0YjAyZjRmMzc3MzFhNjM2ZmFkNWZlNDM0M2MwMWVjNjRjZjg3NDQ3YzE0MDBlNjFjZTk5N2U0NzE0Y2MzIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9kZXBsb3ktY29udGVudC1hZ2VudHMvcmVmZXJlbmNlcy9kZXBsb3ktdGV4dHVyZS1hZ2VudC1kb2NrZXIubWQiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImE4YTdkMzk3MWUzODIyOTc5YWZjYWM2MDc1NDkyOTkwNmI1ZGYwOTcxMDc2MDVhMzA5MTZjMjg1NTkxNjM4ZDYiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2lkZW50aWZ5LWFzc2V0LWNvbnRleHQvUkVBRE1FLm1kIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICIzZTAyMzg5MzFkOGFiZmRjZTk0YWJmMmRmYzFiYzg2YWU4NzdlMzZmMmQzMDJhYjUyYjlmODJlYzc0ODYwYzlhIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9pZGVudGlmeS1hc3NldC1jb250ZXh0L3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICIwMTIzMzgxZDBhNjA4YmJkMzk0Nzk3NGY5NDEzZjdiNjMwYTM2YzEyYzIzNGViMTJhODJjNDkwODQ1YTM3ZDRjIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9pZGVudGlmeS1hc3NldC1jb250ZXh0L3NjcmlwdHMvcmVwb3J0X3NjaGVtYS5qc29uIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICIxNGM2MzYwNzViZDNmNWQyYTQ5ZDVjNTQ3OGJiNjM0ZWU5NjhlMjI3Y2VhMTViZmY0NWYyZjI4YWQyNjFjYTgyIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9pZGVudGlmeS1hc3NldC1jb250ZXh0L3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICJiNjUyNGQ1MzAyNzMyMDQ5ZmFkZWI2ZmUxZmJjZjU5NzMyZmIyOGQ3Y2NlODY5NmJiZDFhMWQ4YTY1MTU5N2ZkIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9udi1jb3JlLXBhY2thZ2Utc2FtcGxlL1JFQURNRS5tZCIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiNjEwMTI3MTM5YTBjMDBkZmUxYzM5NTM4NWVjMjBlNWZjZTJmZWYxZjNkODU3OGE4MDc2MzZlZjFjMjQ2NWNkMyIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvbnYtY29yZS1wYWNrYWdlLXNhbXBsZS9zY3JpcHRzL2NoZWNrX2RlcGVuZGVuY2llcy5weSIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiOGYxNjBmZTkyYWM5YzExMmYwMDc0MzZkZWIyMWIzY2JkYTc5ODEyMTg1MDhkOTcyZDBjNGZjMWIzNzM3MmEyNSIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvbnYtY29yZS1wYWNrYWdlLXNhbXBsZS9zY3JpcHRzL3JlcG9ydF9zY2hlbWEuanNvbiIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiNzc5YTVmNmIxMzkwZjUxMjU0MGQ1M2YxMzE2N2NmNWNkMDI3MTFlZGMwMDNhYmZmODljNTMyYzA2MmRlYmYwNyIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvbnYtY29yZS1wYWNrYWdlLXNhbXBsZS9zY3JpcHRzL3J1bi5weSIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiNGE5MmIzMzZlNTEzNjQ0MjJjYzJkMDdjMzQ0NmQ2NTg5MzI4ODAwNzhhZWYyZmY4NDRmODczYzk1NjM2OGE0ZiIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvbnYtY29yZS1wYWNrYWdlLXNhbXBsZS12YWxpZGF0aW9uL1JFQURNRS5tZCIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiMzBlZGUwMGI4YjYwNmQzYzNjMjNkMDk4N2E5N2RjMmJiMTcxYzAwNDc4ZjhjODU0ZDQ2ZjYzM2U2Yjc3YTEyNSIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvbnYtY29yZS1wYWNrYWdlLXNhbXBsZS12YWxpZGF0aW9uL3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI4ZjE2MGZlOTJhYzljMTEyZjAwNzQzNmRlYjIxYjNjYmRhNzk4MTIxODUwOGQ5NzJkMGM0ZmMxYjM3MzcyYTI1IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9udi1jb3JlLXBhY2thZ2Utc2FtcGxlLXZhbGlkYXRpb24vc2NyaXB0cy9yZXBvcnRfc2NoZW1hLmpzb24iLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogIjc3OWE1ZjZiMTM5MGY1MTI1NDBkNTNmMTMxNjdjZjVjZDAyNzExZWRjMDAzYWJmZjg5YzUzMmMwNjJkZWJmMDciCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL252LWNvcmUtcGFja2FnZS1zYW1wbGUtdmFsaWRhdGlvbi9zY3JpcHRzL3J1bi5weSIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiNGE5MmIzMzZlNTEzNjQ0MjJjYzJkMDdjMzQ0NmQ2NTg5MzI4ODAwNzhhZWYyZmY4NDRmODczYzk1NjM2OGE0ZiIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvb21uaS1hc3NldC12YWxpZGF0ZS9SRUFETUUubWQiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogIjliZjIwYzU3ZjcyN2UxNmVlMWY2NGQ0MWQ1Nzc0NzZhYjQ0ZThkNDkzZjBiNzIzZmE1MWZiY2Q4MjU5ZWM2ZGYiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL29tbmktYXNzZXQtdmFsaWRhdGUvc2NyaXB0cy9jaGVja19kZXBlbmRlbmNpZXMucHkiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogIjA3YTA0ZWI3YzBiNDM0ZjU1MWIwNmQ1YTgxOThhMzEyMmZkN2FiNmViMjdjZmYyNTNjNzZmMjc0MzdjYmJkZmQiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL29tbmktYXNzZXQtdmFsaWRhdGUvc2NyaXB0cy9yZXBvcnRfc2NoZW1hLmpzb24iLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImM1MmJkMTA3OGU2NTFmZWE5NDAzNDhiOWRjYmUwODY3ZjRhMTZhNjNjMzAzM2RlY2ZkY2UyYjFjMmIwNTYzZjkiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL29tbmktYXNzZXQtdmFsaWRhdGUvc2NyaXB0cy9ydW4ucHkiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogIjU5MDhmYThlZTA2MGM1ZWZkNDUxMzcwYTZlOGVjZDJlNWRiYWRhOTYzMDFiYzM4NjI3YTVjOGU0ZTQ2NzJlODciCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL29tbmktYXNzZXQtdmFsaWRhdGUtZ2VvbWV0cnkvUkVBRE1FLm1kIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICIxNjZiZTgyYjFkYzk0MWQwODU5N2E1NjBmNmI3Njg5ZjBkODI5N2FhNDljYmM1MzRmYWQ0YzAyM2Q0YmE4ZjA1IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9vbW5pLWFzc2V0LXZhbGlkYXRlLWdlb21ldHJ5L3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICJiNDUyODJiOTBkN2U2YTU0NTU4YzM1ZjdhMmNmN2E4NzM3MTE4ZjA2ZmQ2YzY5N2QzZmI3OTEwMzhlMzRiOGU3IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9vbW5pLWFzc2V0LXZhbGlkYXRlLWdlb21ldHJ5L3NjcmlwdHMvcmVwb3J0X3NjaGVtYS5qc29uIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICJmZGFmZTIwYmQ4MmUwMzUwNWRkYTAzOTAxOWUyNjU3YzJmZTY2Yjg0MDYzZjZhMjllODRmOWZiM2E2ODBmYzI3IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9vbW5pLWFzc2V0LXZhbGlkYXRlLWdlb21ldHJ5L3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI2ZGZkZjA4ODgyMDg3NTIwNzAxM2IzMTMyNTQ3NDk4NTMxNGQzYzRmZjc1NjVhNjk1N2E0ZTJlYzcxNWJjNTIwIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9vbW5pLWFzc2V0LXZhbGlkYXRlLXBoeXNpY3MvUkVBRE1FLm1kIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI1MzA0NmUxMzg4ZjRiMDg5MmQzYmM1MDQzZDg4NzdkYjMzNGY1YzAwOTdlNjA3ZTZlNjk2OGViMzk2YjNiZTU2IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9vbW5pLWFzc2V0LXZhbGlkYXRlLXBoeXNpY3Mvc2NyaXB0cy9jaGVja19kZXBlbmRlbmNpZXMucHkiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImM3ZjNlOTI4M2U0YzhiYWI0NTg2YTU1NGE2YTFmMjlmOWE2M2ZhYmFjOGViM2E3MmM1MGUxNTk2NzNiZmY2NzIiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL29tbmktYXNzZXQtdmFsaWRhdGUtcGh5c2ljcy9zY3JpcHRzL3JlcG9ydF9zY2hlbWEuanNvbiIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiZTZhMDkxNmQ1YjQwNTEzYzIyNzUyNDFkNTI1MjFiZGFjNTU2YzQyNzNmMDgwZDFmNzQzNmE0NWUwNjdlNzQyNCIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvb21uaS1hc3NldC12YWxpZGF0ZS1waHlzaWNzL3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI0MjVkYWVjOWIxYjc1Y2VkNDBhNzNiZjUxYTcxZjJiOTlmYzdlZWRjYTYzOTVkOGUwMzc4YzkxNTZjNjRhNzI0IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9vdnJ0eC1yZW5kZXItc2VydmljZS9SRUFETUUubWQiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogIjA0N2JiYjQ1ZmRhYzVmZTA1YzUwYWY2ZGI3MDZlNDBiNDg1NWEwNjNlNmViNjM2NjgzYTZhMDkzZDk5YWRlOTUiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL292cnR4LXJlbmRlci1zZXJ2aWNlL3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICIwZjU5MmZlODkwYTc3OTM2YWE3OTdjMTJmYTI2YWU5Nzg2MGVhZDVjMTMyMzVkMWRhYWNmNjk3NTQxZjNiYTg1IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9vdnJ0eC1yZW5kZXItc2VydmljZS9zY3JpcHRzL3JlcG9ydF9zY2hlbWEuanNvbiIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiMzIyZjA4YWRmNDMzNzRjZjUwMjBjYWZmZTAxNTViNDU1ZDY5M2YzYjYxOWJlODc5ZmJjZmUxNWY0OGRjMmFlMyIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvb3ZydHgtcmVuZGVyLXNlcnZpY2Uvc2NyaXB0cy9ydW4ucHkiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImRmZjQzNzYzMzZhYTA0Y2FmNDQ4NTg1YTdmOGY1ZWM0MDE5NzhhYWEyODQ5NjZlYTExNWJjNjMzYzE0YWNjOGIiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL292cnR4LXJlbmRlci1zZXJ2aWNlL3NjcmlwdHMvc3RhZ2VfcHJlcC5weSIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiNzE1NGM3NzVlNmRlYzU2NzQzMjNiMTU4ZjZkNTBhNzQ5ZGJmODI1NmU0NjY2MWUyNTQyZDQ3NTM2M2I2ZjhkZCIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvb3ZydHgtcmVuZGVyLXNlcnZpY2Uvc2NyaXB0cy90dXJudGFibGUucHkiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImI2YmIzNTYxODFkNTBkNGFmYTg4MjI4NDM4NmFjZDUwM2IzN2Y4MWFhZTI3MWExYWNlN2IyOTdiNzE4ZmQ2M2QiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3ByZWZsaWdodC9SRUFETUUubWQiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImFlZGNlYjU5YTAwY2ExMWJhNGYyMGQ3NGFmMjIxMDNiM2JlMTBkYTBhMjdhMmE2YWQyZTljODdmYmM4NDgyM2IiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3ByZWZsaWdodC9zY3JpcHRzL3ByZWZsaWdodC5wczEiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogIjBlNmMyM2U1ZDQyOGNkMWViNTBkNzQzMzRhMWZhMjYyOGNmMjA4ZDQ2MzQ4MjdmNGI0ZTRlOGQyNmM1OGFhMmIiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3ByZWZsaWdodC9zY3JpcHRzL3ByZWZsaWdodC5weSIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiMTEyMjMyYTgxMzc0YTFiZTJlMTE2ZjlkMmM1NDAzZDhiYWFjOGEwNDJkYzIwNWUxN2ZjMzM1NGVlZTQ1MjZmNyIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvcHJlZmxpZ2h0L3NjcmlwdHMvcHJlZmxpZ2h0LnNoIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICIwYjA2OWI3YTY2YjlkM2NhNGIxZWFmZWRiOWYzNGEyODMxODcwNGEwM2U1MjU5MzlhZTYxMDdmYmY2YWEyYzFjIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9zaW1yZWFkeS1jb25mb3JtLXByb2ZpbGUvUkVBRE1FLm1kIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICIwNjg3Zjg2ZDk5ZThiN2FkOTJkMzU3YmM1ZjJmM2MwYmE2NGViNjQ1ZGI1MWJhZGQzZGJmODYxZGViZjY4MjhmIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9zaW1yZWFkeS1jb25mb3JtLXByb2ZpbGUvcmVmZXJlbmNlcy9GRVRfMDAwX0NPUkUvUkVBRE1FLm1kIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI0MTE2OTQxNDJjMGExOWFhNjQ2NDBjNGQ0YWE5ZTdmZDFkMjEzZjk4OWI1OTZkOTEzNDJhNDI1NGMzODAwODE3IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9zaW1yZWFkeS1jb25mb3JtLXByb2ZpbGUvcmVmZXJlbmNlcy9GRVRfMDAwX0NPUkUvc2NyaXB0cy9jaGVja19kZXBlbmRlbmNpZXMucHkiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImFlYjdkYmVlNDMxYTY1NGNhYjhkZDMwNzI4NjRjODVkOGNlZGUyODhlMDdiYzMzZWZiMDM5MmRhM2EyNWZiODUiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3NpbXJlYWR5LWNvbmZvcm0tcHJvZmlsZS9yZWZlcmVuY2VzL0ZFVF8wMDBfQ09SRS9zY3JpcHRzL3JlcG9ydF9zY2hlbWEuanNvbiIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiYTE1OTJiMDZkNGFkYjY0N2UzZGRkMDAwZDIwYzM4M2VmZTAyNGZlZjlkNTkxMmFjMGIzOGMzZTM4NTc1Yzg3OCIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvc2ltcmVhZHktY29uZm9ybS1wcm9maWxlL3JlZmVyZW5jZXMvRkVUXzAwMF9DT1JFL3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICIzMWUyZjY3MzVmMDg2YzA0YmZmYTQ0MmNhNTE3Y2JmMTlhMDdiM2EyN2RhM2NkMWZmNTE2NzBkNjk3MGIxZTIwIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9zaW1yZWFkeS1jb25mb3JtLXByb2ZpbGUvcmVmZXJlbmNlcy9GRVRfMDAxX01JTklNQUwvUkVBRE1FLm1kIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICJjYjNlNTAzMmJkMzI1MDEzNjhhMzg0ZjdmMDg1ZTQxMTA0ZDQ0NWM1ODNlNWUzY2M2ZTUzMjUwYWQxM2M5YWI3IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9zaW1yZWFkeS1jb25mb3JtLXByb2ZpbGUvcmVmZXJlbmNlcy9GRVRfMDAxX01JTklNQUwvc2NyaXB0cy9jaGVja19kZXBlbmRlbmNpZXMucHkiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImZlYTg0YjlhMGFlNzJjM2QxZDgzMjM3MTdiODhjOTEyZTE1OTIxZTZlZjFjYWViOWEzODU5MDI3ODE0YzBlNzMiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3NpbXJlYWR5LWNvbmZvcm0tcHJvZmlsZS9yZWZlcmVuY2VzL0ZFVF8wMDFfTUlOSU1BTC9zY3JpcHRzL3JlcG9ydF9zY2hlbWEuanNvbiIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiZmU4ODc4MDZiNTkyMTAxNzg3ZTBhMDAyNTMzMzM2NzliM2FiNmI1ZWQwZTRlYzc5YzRjZDEwY2MyODRhN2Q0YyIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvc2ltcmVhZHktY29uZm9ybS1wcm9maWxlL3JlZmVyZW5jZXMvRkVUXzAwMV9NSU5JTUFML3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI0MDlmYTkzMjI4YTg5YzMwOTk3MTE4MDZmMzhiYTE4YWJjYWRhODI1YzhlODI1MGM5MmZlNjgzM2IyNWY5NDI5IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9zaW1yZWFkeS1jb25mb3JtLXByb2ZpbGUvcmVmZXJlbmNlcy9GRVRfMDA0X1NJTVVMQVRFX01VTFRJX0JPRFlfUEhZU0lDUy9SRUFETUUubWQiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImRjN2Y1ZjU4NzVmNzE0NDU1YzFmNzljZmJjMTg3MzI1MWQzNDdhZGVlODllYmQxYzUxNjYyNzQxZWM4M2FmZDMiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3NpbXJlYWR5LWNvbmZvcm0tcHJvZmlsZS9yZWZlcmVuY2VzL0ZFVF8wMDRfU0lNVUxBVEVfTVVMVElfQk9EWV9QSFlTSUNTL3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI5M2JiYTBiOGNhMzM3NDE5OTYwYzY2ODFmMDg2MGFmZjA1M2RjNWYzZjIzZGM1NmYxODJlZGQwZWNhNzBjZDkwIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9zaW1yZWFkeS1jb25mb3JtLXByb2ZpbGUvcmVmZXJlbmNlcy9GRVRfMDA0X1NJTVVMQVRFX01VTFRJX0JPRFlfUEhZU0lDUy9zY3JpcHRzL3JlcG9ydF9zY2hlbWEuanNvbiIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiYjQ1Mzk5NWNhMTBlYWEzNTljOTQ5ZDYzZDljNzlkY2ZlOTU4N2Q5NDY5NGMxOWYyN2FmYTllYjA0NmI4MTY0YyIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvc2ltcmVhZHktY29uZm9ybS1wcm9maWxlL3JlZmVyZW5jZXMvRkVUXzAwNF9TSU1VTEFURV9NVUxUSV9CT0RZX1BIWVNJQ1Mvc2NyaXB0cy9ydW4ucHkiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImZhNGY4NjgwNjAwZTlhYjE2MWMyNTcxNDE5YWMxNjE5ZmZlYmFjMzIyZTlkZjU5YjJiNjA0ZWI5NDg0ZDE5NzEiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3NpbXJlYWR5LWNvbmZvcm0tcHJvZmlsZS9yZWZlcmVuY2VzL0ZFVF8wMDVfU0lNVUxBVEVfR1JBU1BfUEhZU0lDUy9SRUFETUUubWQiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogIjZmMDRkZmRhYjYxOTIwZjFiMGRmMmViMWRhOWNhZWY3NWFkYjEwOTM0NTA4Njk1YzZmM2VjZDQ0MmIxOTRmNmEiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3NpbXJlYWR5LWNvbmZvcm0tcHJvZmlsZS9yZWZlcmVuY2VzL0ZFVF8wMDVfU0lNVUxBVEVfR1JBU1BfUEhZU0lDUy9zY3JpcHRzL2F1dGhvcl9ncmFzcF9saW5lLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI4ZTA3MDJmZGYzY2VhZjAyMjg5NTdmY2U5NDg4OGMxMGMwZTcwYjlmMzNkM2RhYjg3Y2UxOWE2MWU0NzliYTU3IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9zaW1yZWFkeS1jb25mb3JtLXByb2ZpbGUvc2NyaXB0cy9jaGVja19kZXBlbmRlbmNpZXMucHkiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogIjRjODJlOWU0NDZiZDYwMDJkNmMzNDA0MjEyNjQ5ZGMyMmU1NDQ3NDQ5NDVlMjE4OGY0ZDcyNjA5OGU3ZTViNmIiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3NpbXJlYWR5LWNvbmZvcm0tcHJvZmlsZS9zY3JpcHRzL3JlcG9ydF9zY2hlbWEuanNvbiIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiMTNjYWZmYzQxNzkwZTJmODk2ODRmNDI1NmQ0MmIzOTc3MzgzNTgxN2RmZGZmYjg5ZDZiOTQ5YmIwZTBkMzQ1NiIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvc2ltcmVhZHktY29uZm9ybS1wcm9maWxlL3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICIxNTM0YmQxODM3YTM1ZTE5MzA5NjUxMjZiMmQ0ZjYwZDcxYjhkMTQxZGY5NmYxOTFmZWYyNGZjZWJkZDNiZDdhIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9zaW1yZWFkeS12YWxpZGF0ZS9SRUFETUUubWQiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImZiN2FmNDAwOTA2YjBhYzVjMjczODMzYzg5MzNiMWQwYjkzZTdlNzRiY2ViMDZjNjhlNGQ5NjQxYTg1MmU3N2UiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3NpbXJlYWR5LXZhbGlkYXRlL3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICIxOThkOTQwZGJhN2JhZjM3MmViNjU1NDQ5NTI5ZGI4ODQyYTk3YzkxOWM0OWU5Mzg2MWM3Mjk3NDRhMDY4ZTkzIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9zaW1yZWFkeS12YWxpZGF0ZS9zY3JpcHRzL3JlcG9ydF9zY2hlbWEuanNvbiIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiYTU2YmYzOTBhMTQ2N2VhNTMwYTBmYjA5YTJiNDAxY2Q2MDQ4ZmFjYzg4MTgxZTExMTM0MTQ1YzhkY2ZjNTI0NiIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvc2ltcmVhZHktdmFsaWRhdGUvc2NyaXB0cy9ydW4ucHkiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogIjRiOTQ2ZDJkNmYxNWYwNzAzM2EwOGUwYzEyNWYxNjFiNDQyYjMyYzhkYTUyZDcyZTFlNmEzMTM3ZmRhYWI4OGUiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3ZhbGlkYXRlLXVzZC1taW5pbXVtL1JFQURNRS5tZCIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiNGJiYzg2OTk2NGYyYjY2NDg1NjAzZDU5MDllMDU0YTZjN2UxOGJkYjdiMjNmYzg5NzExZmRjNTY1ZTI2YmNhMiIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvdmFsaWRhdGUtdXNkLW1pbmltdW0vc2NyaXB0cy9jaGVja19kZXBlbmRlbmNpZXMucHkiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImY4YTJkMzVkYWQxYWRhN2U5ZWJkNjRjODFhODhiODhkZDhmZmEwN2FhZDRkODcxMjUzN2M0MGEwNDAxYzgxODciCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3ZhbGlkYXRlLXVzZC1taW5pbXVtL3NjcmlwdHMvcmVwb3J0X3NjaGVtYS5qc29uIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI4NjlmZDY5NmE5ZWJjMmVmODJmMWNjNzExYzIzMWE1NTM3ZDM2NTI1ZDkzZTk3ZWIwYzlhZWE0YmFhZmRmMWE1IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy92YWxpZGF0ZS11c2QtbWluaW11bS9zY3JpcHRzL3J1bi5weSIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiOWJjMDgwZWE1YTQxNGUxYmIxYjgzNzBiNjA3YzMwMjhlMTBkODBmMDVkNmE0OTRkZjc1MzlkOWM1YzI0OTg0ZCIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvd29ya2Zsb3cubWQiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImU1OWQ5N2FiOGRlMGI0YjFjYzQ0MTlmZWIyNjgwNGY5MmVlM2ZkZjkwNWYyOGRkZmZkNTdiY2Y1NDJmMjdlZGMiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJzaGFyZWQvcHJlZmxpZ2h0X21hbmlmZXN0LnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICIyYzlmMWI2NjhmMTJhZmY0YTU0MTQxNDU4ZWU4MGVjNjI4MmQxZjhmNDk1MDQyMmQ2ZTg4MmMxOWQxNDRlNWNjIgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAic2hhcmVkL3NjcmlwdF91dGlscy5weSIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiN2NiODQ2M2NmYzdiMDk1MWNhYmJkY2Y1MjhhZDgyNjk2MmFjYjAzY2VlY2FmZGUyMjA1YTllZmI5NGQyMGNjYSIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInNoYXJlZC9zaW1yZWFkeV9wYWNrYWdlLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI4YzdlNTgwN2E2MzQ2ZTlhZGVlODY3MDY4NWZlY2Q3M2EzNmZkOGQwNDJhZTVmNTM5MWU2MGI3NDJkNjYyZWU0IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAic2hhcmVkL3NpbXJlYWR5X3BhY2thZ2VfY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI4MTUxODQxMjI5NDlkOGFhOTRmYWZhY2I2MjhlMzRlYWUyM2EwYmNiMjFiOWQ4MWY2YzVhNTBkZDgxNjNkNzM0IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAic2hhcmVkL3NpbXJlYWR5X3BhY2thZ2VfcmVwb3J0X3NjaGVtYS5qc29uIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI3NzlhNWY2YjEzOTBmNTEyNTQwZDUzZjEzMTY3Y2Y1Y2QwMjcxMWVkYzAwM2FiZmY4OWM1MzJjMDYyZGViZjA3IgogICAgICB9LAogICAgICB7CiAgICAgICAgIm5hbWUiOiAic2hhcmVkL3VzZF9jb252ZXJ0X2NhZF9kaWFnbm9zdGljcy5weSIsCiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJkaWdlc3QiOiAiNTYyMzM5NTI4M2E1ODBhMTk1YWU4ZWRiNmJhOWIzMWEzN2VjMzI4MWI1ZmRlYWJhYTJlYjMyN2E0ZjljYTBlMCIKICAgICAgfSwKICAgICAgewogICAgICAgICJuYW1lIjogInNraWxsLWNhcmQubWQiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogIjMwZDk5OTdhNzM1N2NiYTUzNmJmMDVlNTMxOWExMGRjZTJkY2FiODNjYzE3NzcxNTEzOGNiOTI3NzhkMjc0MDIiCiAgICAgIH0KICAgIF0KICB9Cn0=","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MGYCMQCkOsWZFSmcDVFXXU1rOFBh6jIASW4ZmE10sI6panjov8fUNzDBfxc8W6g6qljMIAcCMQC2WH/qQCY58Yug/S3SRTBw76y0DrQt5cIQMXE+BkeUghBOy+5RCPJo/4w8/JQmQoE=","keyid":""}]}} \ No newline at end of file +{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"x509CertificateChain":{"certificates":[{"rawBytes":"MIICgzCCAgmgAwIBAgIUKIyS7SxNteQIiWzK1dWj85E6520wCgYIKoZIzj0EAwMwVTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjEpMCcGA1UEAwwgTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBJQ0EgMDEwHhcNMjYwNDAxMDAwMDAwWhcNMjgwNDIyMTUzMzA5WjBUMQswCQYDVQQGEwJVUzEbMBkGA1UECgwSTlZJRElBIENvcnBvcmF0aW9uMSgwJgYDVQQDDB9OVklESUEgQWdlbnQgU2tpbGxzIFNpZ25pbmcgMDAxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEYoRM9bQl/dGlwSRNi6bTpIJUXH8Nv9GciP6LSflJYYMLCc296kpyuTSsk5ddbAWiDcFX3C/ydX3jwc+qCLYP6uHy9XphyLjOQ27Yb2J6rBLVtRBS1mgGco/Gr7fL6ODco4GaMIGXMB0GA1UdDgQWBBRQ/5ZW3nJ6lmo9SVk7I15o7UGmpTAfBgNVHSMEGDAWgBRPGpILxMBBleJSsBGjrMKsby1CgjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUHMAGGG2h0dHA6Ly9vY3NwLm5kaXMubnZpZGlhLmNvbTAKBggqhkjOPQQDAwNoADBlAjAUygu/GiOCIXrgGr4SmLgeEVDcEitfFUv7ALbvLVGVyMysB3mxmO/uInZfXzWcJZsCMQDxuoxj4ZmO30jhkPIcCxGFCOvnUsnfU3TfGcouYm4M6iRpbKvtVnHPiy4bi6pcKf0="},{"rawBytes":"MIICiDCCAg6gAwIBAgIUZsIuSv9NkpJCNqtYEfCouVv5BzowCgYIKoZIzj0EAwMwUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTAgFw0yNjA0MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowVTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjEpMCcGA1UEAwwgTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBJQ0EgMDEwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASI72cR3ctKGg4VWnB3bNja6g1Z2PnOmFEopkPof+QeIcPk9rT+g9MjJnq51EQXL93a7C2GJ9J985G4o2V85VD7wJ1RaXhluHW2rf3y8bQGeAYaKMr5s/hUgn+M3/9WlWejgaAwgZ0wHQYDVR0OBBYEFE8akgvEwEGV4lKwEaOswqxvLUKCMB8GA1UdIwQYMBaAFItnoAjjfuCEUvzyvWyI2vOGvwPjMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMDcGCCsGAQUFBwEBBCswKTAnBggrBgEFBQcwAYYbaHR0cDovL29jc3AubmRpcy5udmlkaWEuY29tMAoGCCqGSM49BAMDA2gAMGUCMQCeIMMfAbyzPDacw2MxG+Yt1cikrJX/DVxiGfXuHmkkXn6VgSzE79+lkqDErpVO2gYCMCNEColOyvUvkzZGUEI1hQ3PfMgi3FIo9tHoBKMw4/wGBLFpu/0ubtmbBXM6/UMOEw=="},{"rawBytes":"MIICRTCCAcygAwIBAgIUeJdY3rV86EdvFmG7L8LJBsyQFYkwCgYIKoZIzj0EAwMwUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTAgFw0yNjA0MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABAYpiXCDjJ9NT2eSDhyHJVSw1Tbze18cGG2F/578oWvHxg23eQAhNRYdq88i1iOshZSO6C29doKui5Xpmo/7Ctw9Sx4PP2RzOmIuOLCuTdNtKcTRwi4GEsd5BAFvWj42M6NjMGEwHQYDVR0OBBYEFItnoAjjfuCEUvzyvWyI2vOGvwPjMB8GA1UdIwQYMBaAFItnoAjjfuCEUvzyvWyI2vOGvwPjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cAMGQCMCwtAjWLaNwgGWNCgdyNoTyvNhqWRECRJV2r3+7w8g0PL6NHLOsbkgE09BH95h8XlgIwTaQmbbUh2ChAJ5TA1wRiVDnCcvbzHlZl2jM2FcwQQZlk19LOAbyGMRixbu2Ww/rj"}]},"tlogEntries":[]},"dsseEnvelope":{"payload":"ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YxIiwKICAic3ViamVjdCI6IFsKICAgIHsKICAgICAgIm5hbWUiOiAib21uaXZlcnNlLWNhZC10by1zaW1yZWFkeSIsCiAgICAgICJkaWdlc3QiOiB7CiAgICAgICAgInNoYTI1NiI6ICI0MjEwMzA0YzVlNDgxMjdkYmFjNDhkMGRlMTAyOWVkYmJiODFkNDRjNzAwY2Y5NTE5ZmUwNzg0N2Q0MWQ2ZTkwIgogICAgICB9CiAgICB9CiAgXSwKICAicHJlZGljYXRlVHlwZSI6ICJodHRwczovL21vZGVsX3NpZ25pbmcvc2lnbmF0dXJlL3YxLjAiLAogICJwcmVkaWNhdGUiOiB7CiAgICAicmVzb3VyY2VzIjogWwogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogIkJFTkNITUFSSy5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICJjMzZiODQ2YTI3NTYxN2QzOWRmZTBhNjczN2IwOTNhMDViMmQ3ZTYxODBiYjk4YTdiYjhiYjk1OTc1Yzk4ODVlIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogIkNIQU5HRUxPRy5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICI3ZjJlZjhkN2MyMzA5Mzg3YThhZDc5ZDM3OGZlYWRmMTE1YTI0ZjAwMTZlNmY5NTk2YmRkODBjMThmYTVmZTM5IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogIlNLSUxMLm1kIiwKICAgICAgICAiZGlnZXN0IjogIjVlZmEyNWIzNzVkZWU5NzYyNDcwNTRlNzlmY2E4YmIxMGNmMmJkOWJiZDhkNmQ5MWYzMjNjYjg2Mzk1YThiZjAiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAiYWdlbnRzL29wZW5haS55YW1sIiwKICAgICAgICAiZGlnZXN0IjogIjEwYzhiOTc1NjA2ZGYxODZlODc5NmZmNmUxOGY3NmRkOTEwMmJiOGQ0NTI2YTVlZTEwYTRkNWIxOGRmN2RmYTEiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAiY29uZmlnL3NraWxsc3BlY3Rvci1iYXNlbGluZS55YW1sIiwKICAgICAgICAiZGlnZXN0IjogImZjYWY2N2MyZTBiMmFmOTI1MGE1NDFiNDg2ZmI1ZTU5YWFkMDIxNmZhNmEzMTYwOTUxODUzOTM0MThiNzY5ZTAiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAiZXZhbHMvZXZhbHMuanNvbiIsCiAgICAgICAgImRpZ2VzdCI6ICIxOWM3YTY2NDRjNWU0ZGVlMDZhOWIwMThjZjUxNTFkZTZkYmNkOWJlZTQxYWY2NGFhNGI2YjQ0MjFlNWY2NzBkIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogImV2YWxzL2ZpbGVzL21pbmltYWxfbWVzaC5zdGwiLAogICAgICAgICJkaWdlc3QiOiAiNTVhMzIyODE5NTAyZDIwMWQyOGNhOWEyZGMzZmVkYWI4YjNhNTFlMTE0YmVjYmJkOTczYmI1OGY1ZDYzOTZjMSIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2Fzc2VtYmxlLXBhY2thZ2Utc291cmNlL1JFQURNRS5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICJjZmFlODkyMGJlY2QyMDRlM2U2ZjhlZDE2MzQ3OTkzODBhNDQyYTY1YmJjMDc2NjU5YTI4NjdlZTNhNGI3OTc3IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvYXNzZW1ibGUtcGFja2FnZS1zb3VyY2Uvc2NyaXB0cy9jaGVja19kZXBlbmRlbmNpZXMucHkiLAogICAgICAgICJkaWdlc3QiOiAiMzVhY2Q1NjNhNjliOWQwNmNjYmM5YWZkMmM4YjdlMTlhNGJjYzc1ZTM2YWI2NjkzOWY0NmEyODI1ZDgyNDdjMSIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2Fzc2VtYmxlLXBhY2thZ2Utc291cmNlL3NjcmlwdHMvcmVwb3J0X3NjaGVtYS5qc29uIiwKICAgICAgICAiZGlnZXN0IjogImIzNmI0OTQ1NTRiOWYyMGVjMmI0MTU4OTE2YWU1NzY1ZWM1NjI5ZDZiNzk4YmZmODAzMDRlZjU4YWY2ODc0NjYiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9hc3NlbWJsZS1wYWNrYWdlLXNvdXJjZS9zY3JpcHRzL3J1bi5weSIsCiAgICAgICAgImRpZ2VzdCI6ICI3ZTBkZDMwNWNkMGUwODk4OTkzNmUxNTIyZTFmMGZjYmE1OTNhZjhiMzhmZDUwZDE5NjRkZjAyNDdmNjZhMWUxIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29tbWFuZHMubWQiLAogICAgICAgICJkaWdlc3QiOiAiZDU2ZjU3NjY3MDlmYTMzNDc3YjcxODI4MDY4NDI0Y2Y4YmRlMzc3OWNiN2NlYWQ5ZDk0MDYzZTNmZTlhYzBkZiIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnRlbnQtYWdlbnRzL1JFQURNRS5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICI2MjExODJlYWI2Y2YzMWEwZjcyMmM1NmE4OWE2YWQzOWNmOTEzNTU2YTg3NzkzYzlhYzI4YmQzMmRkMGZkNTc5IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udGVudC1hZ2VudHMvcmVmZXJlbmNlcy9tYXRlcmlhbC1hZ2VudC1jbGllbnQvUkVBRE1FLm1kIiwKICAgICAgICAiZGlnZXN0IjogIjhhNTJmMmI1NWY0NDc1NjU5ZjNjOGNiODFlMzBmMjMzMmQ3OGI4ODRjYmEwMTk4MTJkNDhiZGNjZDU2NDQ4ZDQiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb250ZW50LWFnZW50cy9yZWZlcmVuY2VzL21hdGVyaWFsLWFnZW50LWNsaWVudC9zY3JpcHRzL2NoZWNrX2RlcGVuZGVuY2llcy5weSIsCiAgICAgICAgImRpZ2VzdCI6ICJiMDA4NzMzZWM2NWZkYzhhZDdhOWQ5ZGQ4ZGQzZjE2ZGYyMTYzZTMwOWJiYzJiYjdiMjFlYWQ4ZDQyYWIzMjE2IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udGVudC1hZ2VudHMvcmVmZXJlbmNlcy9tYXRlcmlhbC1hZ2VudC1jbGllbnQvc2NyaXB0cy9yZXBvcnRfc2NoZW1hLmpzb24iLAogICAgICAgICJkaWdlc3QiOiAiY2RjODkzMTYxYjY5MDEyY2QyMTdmYWFhYjIzNzkwMWNkMDc2Mzk4MmZlNjJkY2JlMzM3MTJiYTQ5NmZhZWU0ZiIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnRlbnQtYWdlbnRzL3JlZmVyZW5jZXMvbWF0ZXJpYWwtYWdlbnQtY2xpZW50L3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiZGlnZXN0IjogImNkOTkzNDk1ODE3Y2I0OGVjZWNmZDE5M2IwMGI5MWJmNDVmN2ZhM2ZlZWNlYjYzOWFlMjhjODgxMDBjODEyODUiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb250ZW50LWFnZW50cy9yZWZlcmVuY2VzL3BoeXNpY3MtYWdlbnQtY2xpZW50L1JFQURNRS5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICIzMzkzZDU2ZDE4ZmM0NWQzNmNlNDEzYjRjMWM2MjQyODIyOGQ1OGUzNzQyYjk4NWQ4YzVkMTIxODExYzI1MWIxIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udGVudC1hZ2VudHMvcmVmZXJlbmNlcy9waHlzaWNzLWFnZW50LWNsaWVudC9zY3JpcHRzL2NoZWNrX2RlcGVuZGVuY2llcy5weSIsCiAgICAgICAgImRpZ2VzdCI6ICJiMDA4NzMzZWM2NWZkYzhhZDdhOWQ5ZGQ4ZGQzZjE2ZGYyMTYzZTMwOWJiYzJiYjdiMjFlYWQ4ZDQyYWIzMjE2IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udGVudC1hZ2VudHMvcmVmZXJlbmNlcy9waHlzaWNzLWFnZW50LWNsaWVudC9zY3JpcHRzL3JlcG9ydF9zY2hlbWEuanNvbiIsCiAgICAgICAgImRpZ2VzdCI6ICJjZGM4OTMxNjFiNjkwMTJjZDIxN2ZhYWFiMjM3OTAxY2QwNzYzOTgyZmU2MmRjYmUzMzcxMmJhNDk2ZmFlZTRmIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udGVudC1hZ2VudHMvcmVmZXJlbmNlcy9waHlzaWNzLWFnZW50LWNsaWVudC9zY3JpcHRzL3J1bi5weSIsCiAgICAgICAgImRpZ2VzdCI6ICJjZDk5MzQ5NTgxN2NiNDhlY2VjZmQxOTNiMDBiOTFiZjQ1ZjdmYTNmZWVjZWI2MzlhZTI4Yzg4MTAwYzgxMjg1IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udGVudC1hZ2VudHMvcmVmZXJlbmNlcy90ZXh0dXJlLWFnZW50LWNsaWVudC9SRUFETUUubWQiLAogICAgICAgICJkaWdlc3QiOiAiYTM2ZDAwZTRmMzBlMTFiMWJlZWYwMzVjNTVjNTc4M2JiYmMwYjY2MjNmMzEzZDUxZGU5ZTdlN2QxYjI3Yzg5MyIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnRlbnQtYWdlbnRzL3JlZmVyZW5jZXMvdGV4dHVyZS1hZ2VudC1jbGllbnQvc2NyaXB0cy9jaGVja19kZXBlbmRlbmNpZXMucHkiLAogICAgICAgICJkaWdlc3QiOiAiYjAwODczM2VjNjVmZGM4YWQ3YTlkOWRkOGRkM2YxNmRmMjE2M2UzMDliYmMyYmI3YjIxZWFkOGQ0MmFiMzIxNiIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnRlbnQtYWdlbnRzL3JlZmVyZW5jZXMvdGV4dHVyZS1hZ2VudC1jbGllbnQvc2NyaXB0cy9yZXBvcnRfc2NoZW1hLmpzb24iLAogICAgICAgICJkaWdlc3QiOiAiY2RjODkzMTYxYjY5MDEyY2QyMTdmYWFhYjIzNzkwMWNkMDc2Mzk4MmZlNjJkY2JlMzM3MTJiYTQ5NmZhZWU0ZiIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnRlbnQtYWdlbnRzL3JlZmVyZW5jZXMvdGV4dHVyZS1hZ2VudC1jbGllbnQvc2NyaXB0cy9ydW4ucHkiLAogICAgICAgICJkaWdlc3QiOiAiY2Q5OTM0OTU4MTdjYjQ4ZWNlY2ZkMTkzYjAwYjkxYmY0NWY3ZmEzZmVlY2ViNjM5YWUyOGM4ODEwMGM4MTI4NSIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnRlbnQtYWdlbnRzL3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiZGlnZXN0IjogIjg3MmRjZTFmOWNmY2Q5NDAwZTBiNzQzOGI1NTkyNzBjZDg5YTkxNjI3YmQ4ZjUzNGIzYjIwMzFkMzQ2YzEwZTEiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb250ZW50LWFnZW50cy9zY3JpcHRzL2NvbnRlbnRfYWdlbnRfY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiZGlnZXN0IjogImQ4MGMzYTIyZmU1MDNkNDhkYTA3YjI0MDc5NmYwYTI4Mzg4MDIxMDMyNGE2ZDQ1NDFkNjliYTllNGE4MzI5NmEiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb250ZW50LWFnZW50cy9zY3JpcHRzL2NvbnRlbnRfYWdlbnRfY2xpZW50LnB5IiwKICAgICAgICAiZGlnZXN0IjogImJjNTc3OWVkZTM2OTZlMTBiM2Q5NzJlMTA4MmQ0YjY5YTBkMjMwZDBjMmQxYmM5ODAzMWMxMmM5MWI4Zjc1M2QiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb250ZW50LWFnZW50cy9zY3JpcHRzL2NvbnRlbnRfYWdlbnRfbWF0ZXJpYWxfY2xlYW51cC5weSIsCiAgICAgICAgImRpZ2VzdCI6ICI2MDMxODUyY2NlOGQ3MDMzYjdjOGQ3ZWExYjM5NmQ3NzJhYjQ5ZGFhMmI0ZjA0OGMzMjg5ZDA3YWRhZDQ2ZDgzIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udGVudC1hZ2VudHMvc2NyaXB0cy9jb250ZW50X2FnZW50X3JlcG9ydF9zY2hlbWEuanNvbiIsCiAgICAgICAgImRpZ2VzdCI6ICJjZGM4OTMxNjFiNjkwMTJjZDIxN2ZhYWFiMjM3OTAxY2QwNzYzOTgyZmU2MmRjYmUzMzcxMmJhNDk2ZmFlZTRmIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udGVudC1hZ2VudHMvc2NyaXB0cy9yZXBvcnRfc2NoZW1hLmpzb24iLAogICAgICAgICJkaWdlc3QiOiAiYjVhMTdlMjNjMzEzNWU5OTdiZDdkYmE5ZGJlZjY0NzMwMjJjYzU1OWVjMDg5MTE2YmQ1ZmQ4MGFhMmYwYThmOCIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnRlbnQtYWdlbnRzL3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiZGlnZXN0IjogImQ2ZWQzOTM0OWZhY2ViY2RjMzdmZGU0YzQzN2I0NmUyNGI3YmQ3ZDY4MDNjOGY3NzkyOTlkY2NkMDU3YWFiMjkiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9SRUFETUUubWQiLAogICAgICAgICJkaWdlc3QiOiAiN2RiNDE2NjZkZTU2MGMzZWI5N2Q5ZGM0MDA3ODVlNTMwNTg0NDBjOWI4N2UxYTA3ZDc3OTBhMGVjNzI1YzgyNCIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnZlcnQtdG8tdXNkL3JlZmVyZW5jZXMvbXVqb2NvLXVzZC1jb252ZXJ0ZXIvUkVBRE1FLm1kIiwKICAgICAgICAiZGlnZXN0IjogIjA1YjcyYzJkMDJhNzI0ZWEwODVjOGE2OTgxNjM3MGEwZGQ4OWUzYmZjODg0ZDNiOTY1ZTg2N2JhZmZkYTMxNDQiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9yZWZlcmVuY2VzL211am9jby11c2QtY29udmVydGVyL3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiZGlnZXN0IjogIjYyNzE2YmYzNmVhYWFlNzgwODVmMDI3YTdjYzhlYzA4M2NmY2VmNjU5ODg3MzNkZGQ5YWY5ZjA1MmEwMzdmYTYiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9yZWZlcmVuY2VzL211am9jby11c2QtY29udmVydGVyL3NjcmlwdHMvcmVwb3J0X3NjaGVtYS5qc29uIiwKICAgICAgICAiZGlnZXN0IjogImIyZjAxOGM1ZmVjYmM5YWNmOTJmYWM5NGMxYWYzZGZhMDVhNzk1MmRlZWI1N2YyYjBjODhlMDdiNmUxYTJlYjYiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9yZWZlcmVuY2VzL211am9jby11c2QtY29udmVydGVyL3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiZGlnZXN0IjogImIzMmE4MDVhOTdlODQxNWFhMDk4ZTcxOTY0MmNkZDYwNzZkMzE0N2U1YmQ0MmExNTM0ODRlMDU0Y2JlZDA1ZDQiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9yZWZlcmVuY2VzL29wZW51c2QtZXhjaGFuZ2Utc2RrLXVzZGV4L1JFQURNRS5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICJmYzAzOTIyZTJmNWRjMWMxYzRjZGQ0YzA4MWJhNWVhODQxNGE3ZTZiYjE4YjQ0ODI4YWJiNDNmOTAzZmE0MTI1IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udmVydC10by11c2QvcmVmZXJlbmNlcy91cmRmLXVzZC1jb252ZXJ0ZXIvUkVBRE1FLm1kIiwKICAgICAgICAiZGlnZXN0IjogIjk4NmEzMTE0MjI4ODg0NTQwNmZhZTM2ODM1MGQ2MjcxZjZjOWJhMjc5MTA2NGRkODIxNjNjY2MzN2I3YzRkMDgiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9yZWZlcmVuY2VzL3VyZGYtdXNkLWNvbnZlcnRlci9zY3JpcHRzL2NoZWNrX2RlcGVuZGVuY2llcy5weSIsCiAgICAgICAgImRpZ2VzdCI6ICI3NWM1MDMwMDZhNGI4MzM4M2YzZDI5OTE3YWQxNzExODgxMzcwOTY3ZWY0MzVmZWM4MjNlMmM2MzNmNTM3NmI3IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udmVydC10by11c2QvcmVmZXJlbmNlcy91cmRmLXVzZC1jb252ZXJ0ZXIvc2NyaXB0cy9yZXBvcnRfc2NoZW1hLmpzb24iLAogICAgICAgICJkaWdlc3QiOiAiNDc1YmEzMzFjNGZjOGFjOTkxOTI5ZjUyZDgyOTczYzZiOTVlMDIyOTI1NWM2OTRmY2ZmNjkzMTcxMzkxYjcyYiIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnZlcnQtdG8tdXNkL3JlZmVyZW5jZXMvdXJkZi11c2QtY29udmVydGVyL3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiZGlnZXN0IjogIjc0ZjdlOGRlNmMxMjFjNjgyNDdkZDE3OWIyMTlmMDQ1Nzk1ZGQ0ZDI2MTI2ZDJhNWU3ZTMyNmNiZmUyZDZlNDEiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9yZWZlcmVuY2VzL3VzZC1jb252ZXJ0LWNhZC9SRUFETUUubWQiLAogICAgICAgICJkaWdlc3QiOiAiOTkwNDE2YTE0Yjc5MGRiNDEwMTM3ZjgyMjY3M2Y0NDdjNzBiMmIyYWExYzVmMTUxMmJiY2E0MmNjN2VkODJjZCIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnZlcnQtdG8tdXNkL3JlZmVyZW5jZXMvdXNkLWNvbnZlcnQtY2FkL3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiZGlnZXN0IjogIjY3MmUzNDE2NjI4ZjNlYjY0ZDMxYWViZWEwNjVhYmEwM2JlYTFlOWY0MDE5ODRiYTdkMTc2YzY0ZjQ5YmNjNWMiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9yZWZlcmVuY2VzL3VzZC1jb252ZXJ0LWNhZC9zY3JpcHRzL3JlcG9ydF9zY2hlbWEuanNvbiIsCiAgICAgICAgImRpZ2VzdCI6ICI3M2I3MjQzYWU4ZjJhYWEyMTMzZmEzYzUyMjJmZWVlYTBjNGI0ZjVjZGY4ZWFkNDUwMzQ3N2Y1NDc4ZGQ5ZDgxIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udmVydC10by11c2QvcmVmZXJlbmNlcy91c2QtY29udmVydC1jYWQvc2NyaXB0cy9ydW4ucHkiLAogICAgICAgICJkaWdlc3QiOiAiY2Y3ZWYzOWFkZmJkNDk4MjlkZTcxZjgwMGY1YjkzMmIwMzFlNzNkZTg2OGM1NjcwZTg0YjVkZDNiNWU5NTc5YiIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnZlcnQtdG8tdXNkL3JlZmVyZW5jZXMvdXNkLWNvbnZlcnQtZ3NwbGF0L1JFQURNRS5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICI3MzcxYzEzYmU1NDQ0OTZhMzFlNDE2YWU0MGQ4YTE2ODM2M2IxNjExZmEwYzI0Mjc0MzQ5OGU4NDJlNjVhMDk0IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udmVydC10by11c2QvcmVmZXJlbmNlcy91c2QtY29udmVydC1nc3BsYXQvc2NyaXB0cy9jaGVja19kZXBlbmRlbmNpZXMucHkiLAogICAgICAgICJkaWdlc3QiOiAiNGExMzQ2ODkwNzljYTNlNzc5MTkwOWI3Y2Q0YTEzZWYyMzVkNmM1OWEyMzc5NWM4YzVkNzUxZWExNjY0Zjk4NSIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnZlcnQtdG8tdXNkL3JlZmVyZW5jZXMvdXNkLWNvbnZlcnQtZ3NwbGF0L3NjcmlwdHMvcmVwb3J0X3NjaGVtYS5qc29uIiwKICAgICAgICAiZGlnZXN0IjogImM0MzMzNjBmZmI4ZWNhZjZhZGNhNDAwZDRmN2M4ZDFhMGRhOTA4OWFkM2E2NjU3M2Q0MTlmMmRlMjg2NGE0OGYiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9yZWZlcmVuY2VzL3VzZC1jb252ZXJ0LWdzcGxhdC9zY3JpcHRzL3J1bi5weSIsCiAgICAgICAgImRpZ2VzdCI6ICI1N2VlMTAxYTA5ODAzNWFjYzQxOTBjMWU4NTlkYmU3NGMxN2FlNDE1ZmNjMjUxM2U2ZjZjMmQzZjU4ZDY1YTUyIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvY29udmVydC10by11c2Qvc2NyaXB0cy9jaGVja19kZXBlbmRlbmNpZXMucHkiLAogICAgICAgICJkaWdlc3QiOiAiZDVmNjU4OTJhNzcwZjM3ZDJiNWYzM2M4NzA2YjI1Y2NiNTRmY2IwMjZkYThiZmZiYjlkMjk0NTk5MjUwMWJhNyIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2NvbnZlcnQtdG8tdXNkL3NjcmlwdHMvcmVwb3J0X3NjaGVtYS5qc29uIiwKICAgICAgICAiZGlnZXN0IjogIjIyZWE4NDQ3NjliYmRiNGE2MzEwZDMzYzlhY2ExZGZlODZlM2YzNWM5MDQ5MGQ5ZTljZDdkOTFhZDY5YTJhMjEiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9jb252ZXJ0LXRvLXVzZC9zY3JpcHRzL3J1bi5weSIsCiAgICAgICAgImRpZ2VzdCI6ICJkMWJiMjFiN2QyOTgxZGE2ODQzZGViY2IxZDEyMWI3NGIyMGNhOThkMzhkZmRjMzAzMjhkNzc3YTI1ZTliY2ExIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvZGVwbG95LWNvbnRlbnQtYWdlbnRzL1JFQURNRS5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICI3NjE2YjI4OWEwODEwMDY2MTRlNTExZjRkYzUzYTlkODVkYjU1YTJlZjVmYjkzYzZhM2ZkNjRiODljNzIzMTE5IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvZGVwbG95LWNvbnRlbnQtYWdlbnRzL3JlZmVyZW5jZXMvZGVwbG95LW1hdGVyaWFsLWFnZW50LWRvY2tlci5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICI1ODEzYmM2MGJlM2Y4YjVjOWZiZjYzNjE4N2MzNzU4MjM4NmE2OGY2Y2E4NDFjYTBlODg0OTZlNTg4NGQ2NzhkIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvZGVwbG95LWNvbnRlbnQtYWdlbnRzL3JlZmVyZW5jZXMvZGVwbG95LW92cnR4LWRvY2tlci5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICJhOWFkYzFlZmQwMmQ4MTNmYWI3MjIyM2QwNzkxZWQ0YTJmMzc2MTQ5ZjZkZDQzZjE1ZjhkZmNjNWEwMTZmYTIxIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvZGVwbG95LWNvbnRlbnQtYWdlbnRzL3JlZmVyZW5jZXMvZGVwbG95LXBoeXNpY3MtYWdlbnQtZG9ja2VyLm1kIiwKICAgICAgICAiZGlnZXN0IjogImY5NDViODg1M2ZiYmI2ZTIyZjRhNzM1MGE0NGM2MDY5OWM3MWJmNmIwYjVjMzQxM2RkZDEyZWU5ZmMyZDk5MTMiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9kZXBsb3ktY29udGVudC1hZ2VudHMvcmVmZXJlbmNlcy9kZXBsb3ktdGV4dHVyZS1hZ2VudC1kb2NrZXIubWQiLAogICAgICAgICJkaWdlc3QiOiAiNGZjMTQ2MjQxZTc3ZWIzZTk0OTBkOGRkZDNlZWE2MWY5OWMyNGQzMzlkODU3NGI5YjU4ODAzYzg0ODk0ZWI0YSIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL2lkZW50aWZ5LWFzc2V0LWNvbnRleHQvUkVBRE1FLm1kIiwKICAgICAgICAiZGlnZXN0IjogIjNlMDIzODkzMWQ4YWJmZGNlOTRhYmYyZGZjMWJjODZhZTg3N2UzNmYyZDMwMmFiNTJiOWY4MmVjNzQ4NjBjOWEiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9pZGVudGlmeS1hc3NldC1jb250ZXh0L3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiZGlnZXN0IjogIjAxMjMzODFkMGE2MDhiYmQzOTQ3OTc0Zjk0MTNmN2I2MzBhMzZjMTJjMjM0ZWIxMmE4MmM0OTA4NDVhMzdkNGMiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9pZGVudGlmeS1hc3NldC1jb250ZXh0L3NjcmlwdHMvcmVwb3J0X3NjaGVtYS5qc29uIiwKICAgICAgICAiZGlnZXN0IjogIjE0YzYzNjA3NWJkM2Y1ZDJhNDlkNWM1NDc4YmI2MzRlZTk2OGUyMjdjZWExNWJmZjQ1ZjJmMjhhZDI2MWNhODIiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9pZGVudGlmeS1hc3NldC1jb250ZXh0L3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiZGlnZXN0IjogImI2NTI0ZDUzMDI3MzIwNDlmYWRlYjZmZTFmYmNmNTk3MzJmYjI4ZDdjY2U4Njk2YmJkMWExZDhhNjUxNTk3ZmQiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9udi1jb3JlLXBhY2thZ2Utc2FtcGxlL1JFQURNRS5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICJkZjBiMWVkN2RhYzY4ZTdiOWMwZDRiMDQ0ODk1MzE0MDQxMzRlMjcxY2E5ZThkNTU2MDE2NWJhYjRhYmI2ZmNlIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvbnYtY29yZS1wYWNrYWdlLXNhbXBsZS9zY3JpcHRzL2NoZWNrX2RlcGVuZGVuY2llcy5weSIsCiAgICAgICAgImRpZ2VzdCI6ICI4ZjE2MGZlOTJhYzljMTEyZjAwNzQzNmRlYjIxYjNjYmRhNzk4MTIxODUwOGQ5NzJkMGM0ZmMxYjM3MzcyYTI1IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvbnYtY29yZS1wYWNrYWdlLXNhbXBsZS9zY3JpcHRzL3JlcG9ydF9zY2hlbWEuanNvbiIsCiAgICAgICAgImRpZ2VzdCI6ICI3NzlhNWY2YjEzOTBmNTEyNTQwZDUzZjEzMTY3Y2Y1Y2QwMjcxMWVkYzAwM2FiZmY4OWM1MzJjMDYyZGViZjA3IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvbnYtY29yZS1wYWNrYWdlLXNhbXBsZS9zY3JpcHRzL3J1bi5weSIsCiAgICAgICAgImRpZ2VzdCI6ICI0YTkyYjMzNmU1MTM2NDQyMmNjMmQwN2MzNDQ2ZDY1ODkzMjg4MDA3OGFlZjJmZjg0NGY4NzNjOTU2MzY4YTRmIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvbnYtY29yZS1wYWNrYWdlLXNhbXBsZS12YWxpZGF0aW9uL1JFQURNRS5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICIwNGE2MmYyMjVjNzNjZGZjYjczNGQ2MDk2OTlhOTI3YmI3OTlhODg5ZDFjM2JmNDRjNjFmZTNhNjg4YTdjNmU1IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvbnYtY29yZS1wYWNrYWdlLXNhbXBsZS12YWxpZGF0aW9uL3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiZGlnZXN0IjogIjhmMTYwZmU5MmFjOWMxMTJmMDA3NDM2ZGViMjFiM2NiZGE3OTgxMjE4NTA4ZDk3MmQwYzRmYzFiMzczNzJhMjUiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9udi1jb3JlLXBhY2thZ2Utc2FtcGxlLXZhbGlkYXRpb24vc2NyaXB0cy9yZXBvcnRfc2NoZW1hLmpzb24iLAogICAgICAgICJkaWdlc3QiOiAiNzc5YTVmNmIxMzkwZjUxMjU0MGQ1M2YxMzE2N2NmNWNkMDI3MTFlZGMwMDNhYmZmODljNTMyYzA2MmRlYmYwNyIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL252LWNvcmUtcGFja2FnZS1zYW1wbGUtdmFsaWRhdGlvbi9zY3JpcHRzL3J1bi5weSIsCiAgICAgICAgImRpZ2VzdCI6ICI0YTkyYjMzNmU1MTM2NDQyMmNjMmQwN2MzNDQ2ZDY1ODkzMjg4MDA3OGFlZjJmZjg0NGY4NzNjOTU2MzY4YTRmIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvb21uaS1hc3NldC12YWxpZGF0ZS9SRUFETUUubWQiLAogICAgICAgICJkaWdlc3QiOiAiOWJmMjBjNTdmNzI3ZTE2ZWUxZjY0ZDQxZDU3NzQ3NmFiNDRlOGQ0OTNmMGI3MjNmYTUxZmJjZDgyNTllYzZkZiIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL29tbmktYXNzZXQtdmFsaWRhdGUvc2NyaXB0cy9jaGVja19kZXBlbmRlbmNpZXMucHkiLAogICAgICAgICJkaWdlc3QiOiAiYzI0MDFkZWRmZDljZTllMGI5ODEyYTQwZDQ0YjcyODk2YjM5N2FlMDk5MTQzY2NmZmYyNTg3MjE0ODU4MGM3YiIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL29tbmktYXNzZXQtdmFsaWRhdGUvc2NyaXB0cy9yZXBvcnRfc2NoZW1hLmpzb24iLAogICAgICAgICJkaWdlc3QiOiAiYzUyYmQxMDc4ZTY1MWZlYTk0MDM0OGI5ZGNiZTA4NjdmNGExNmE2M2MzMDMzZGVjZmRjZTJiMWMyYjA1NjNmOSIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL29tbmktYXNzZXQtdmFsaWRhdGUvc2NyaXB0cy9ydW4ucHkiLAogICAgICAgICJkaWdlc3QiOiAiZWI0ZWZhNmEzMjU4M2I0OTA4Y2QzYzI3OWNmYzE1N2MzZWVlYmQxZmQ1OTg4ZGI2M2JlNmMwOWI4YzZlYzUwYyIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL29tbmktYXNzZXQtdmFsaWRhdGUtZ2VvbWV0cnkvUkVBRE1FLm1kIiwKICAgICAgICAiZGlnZXN0IjogIjE2NmJlODJiMWRjOTQxZDA4NTk3YTU2MGY2Yjc2ODlmMGQ4Mjk3YWE0OWNiYzUzNGZhZDRjMDIzZDRiYThmMDUiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9vbW5pLWFzc2V0LXZhbGlkYXRlLWdlb21ldHJ5L3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiZGlnZXN0IjogIjc5NDhhYWExMTFlOGFhMjlkMGQxMzIyZTYwZGY0MDE1YzRkZTBmZjNkOTllZjJkZjdkNzY2ZWJjNzg4YmU3NDgiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9vbW5pLWFzc2V0LXZhbGlkYXRlLWdlb21ldHJ5L3NjcmlwdHMvcmVwb3J0X3NjaGVtYS5qc29uIiwKICAgICAgICAiZGlnZXN0IjogImZkYWZlMjBiZDgyZTAzNTA1ZGRhMDM5MDE5ZTI2NTdjMmZlNjZiODQwNjNmNmEyOWU4NGY5ZmIzYTY4MGZjMjciCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9vbW5pLWFzc2V0LXZhbGlkYXRlLWdlb21ldHJ5L3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiZGlnZXN0IjogIjZkZmRmMDg4ODIwODc1MjA3MDEzYjMxMzI1NDc0OTg1MzE0ZDNjNGZmNzU2NWE2OTU3YTRlMmVjNzE1YmM1MjAiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9vbW5pLWFzc2V0LXZhbGlkYXRlLXBoeXNpY3MvUkVBRE1FLm1kIiwKICAgICAgICAiZGlnZXN0IjogIjUzMDQ2ZTEzODhmNGIwODkyZDNiYzUwNDNkODg3N2RiMzM0ZjVjMDA5N2U2MDdlNmU2OTY4ZWIzOTZiM2JlNTYiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9vbW5pLWFzc2V0LXZhbGlkYXRlLXBoeXNpY3Mvc2NyaXB0cy9jaGVja19kZXBlbmRlbmNpZXMucHkiLAogICAgICAgICJkaWdlc3QiOiAiNDMwNTdmNjJlMzllZmUxYTQ0NDE4NDUwYTYyODc1MTExODJlYWY1ZjE5NGQzYmFhMWE0M2YwODUxNzczZGY2MSIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL29tbmktYXNzZXQtdmFsaWRhdGUtcGh5c2ljcy9zY3JpcHRzL3JlcG9ydF9zY2hlbWEuanNvbiIsCiAgICAgICAgImRpZ2VzdCI6ICJlNmEwOTE2ZDViNDA1MTNjMjI3NTI0MWQ1MjUyMWJkYWM1NTZjNDI3M2YwODBkMWY3NDM2YTQ1ZTA2N2U3NDI0IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvb21uaS1hc3NldC12YWxpZGF0ZS1waHlzaWNzL3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiZGlnZXN0IjogIjQyNWRhZWM5YjFiNzVjZWQ0MGE3M2JmNTFhNzFmMmI5OWZjN2VlZGNhNjM5NWQ4ZTAzNzhjOTE1NmM2NGE3MjQiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9vdnJ0eC1yZW5kZXItc2VydmljZS9SRUFETUUubWQiLAogICAgICAgICJkaWdlc3QiOiAiMTM2ZDJjMWQyNzFiYjkyMDQ0YjdhZWI0NWQ1OGEwNzMxYmI3ODVjM2VjYzA4MDJmMjQ1YmY3NmJmYTJjNzk0ZCIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL292cnR4LXJlbmRlci1zZXJ2aWNlL3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiZGlnZXN0IjogImU2MjFkNmJjNTY1NTE1MmE2YjMyNGZmNDdhOTM5YjM0MTA2MmQ1YzVkNTc4YTEwNWE4ZjYxZGZiZTdmZGRjZjUiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9vdnJ0eC1yZW5kZXItc2VydmljZS9zY3JpcHRzL3JlcG9ydF9zY2hlbWEuanNvbiIsCiAgICAgICAgImRpZ2VzdCI6ICIzMjJmMDhhZGY0MzM3NGNmNTAyMGNhZmZlMDE1NWI0NTVkNjkzZjNiNjE5YmU4NzlmYmNmZTE1ZjQ4ZGMyYWUzIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvb3ZydHgtcmVuZGVyLXNlcnZpY2Uvc2NyaXB0cy9ydW4ucHkiLAogICAgICAgICJkaWdlc3QiOiAiOTQzY2ExNTNkNjAwOWY1YWUyOTA0MmQ4M2Y2NzRkNDM5YmIxNWY4NmFlMmM4YzZjMDViNDAzOGY5MGFmYTI0OCIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL292cnR4LXJlbmRlci1zZXJ2aWNlL3NjcmlwdHMvc3RhZ2VfcHJlcC5weSIsCiAgICAgICAgImRpZ2VzdCI6ICI2NjZhNmQzOTQwODFkMjRmODQyY2ZiNzA4OTk5NmI2MjY3OTY5MjNhOTFhNDk5N2I1ZmUxZTVkZmY4OTA5NzZjIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvb3ZydHgtcmVuZGVyLXNlcnZpY2Uvc2NyaXB0cy90dXJudGFibGUucHkiLAogICAgICAgICJkaWdlc3QiOiAiY2M0NDdiMGRmNmE5MTEyZGE4NTA0ZGMzY2NiMmYyMGUzZDk4MzNmNWYzYjE5NWI2ZDUzOGFlNDlhZGE2N2I3OCIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3ByZWZsaWdodC9SRUFETUUubWQiLAogICAgICAgICJkaWdlc3QiOiAiOTU4NTc2ZDY2MWQ5MGEzZThlYzQ1NjQ5MjM2YWVlNTJiOGQ0ZmYyYTA3NDg4NzE4Yzg3YmE5NTE3NzAxMzcxMyIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3ByZWZsaWdodC9zY3JpcHRzL3ByZWZsaWdodC5wczEiLAogICAgICAgICJkaWdlc3QiOiAiMGU2YzIzZTVkNDI4Y2QxZWI1MGQ3NDMzNGExZmEyNjI4Y2YyMDhkNDYzNDgyN2Y0YjRlNGU4ZDI2YzU4YWEyYiIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3ByZWZsaWdodC9zY3JpcHRzL3ByZWZsaWdodC5weSIsCiAgICAgICAgImRpZ2VzdCI6ICI5NjY4NmEwNjgyMzZjNDdjMWE3MDJkMjMzNGI2NzYxYzJjNWMzMTE3ZTY2ZGQwOGIwZWM0ZWZjNjU2NTZmZjgwIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvcHJlZmxpZ2h0L3NjcmlwdHMvcHJlZmxpZ2h0LnNoIiwKICAgICAgICAiZGlnZXN0IjogIjBiMDY5YjdhNjZiOWQzY2E0YjFlYWZlZGI5ZjM0YTI4MzE4NzA0YTAzZTUyNTkzOWFlNjEwN2ZiZjZhYTJjMWMiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9wdWJsaXNoaW5nLWxheW91dC5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICJhZGQ1NjBlNmUxZTdkNmI4OWUyNTZhMzlmZGJhMThmYTQ2N2Q0YzA4NmNlN2RiM2YzNTg4OTllMjllZTQyZTc2IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvc2ltcmVhZHktY29uZm9ybS1wcm9maWxlL1JFQURNRS5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICJkNjljN2ZhNmY2ZDUzODIxMTliODA1NjEzNDliMDBmNzkzOWU4ZDQyYmExMGJjYmM0NGI3YWY0OTUwYmYwNzRkIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvc2ltcmVhZHktY29uZm9ybS1wcm9maWxlL3JlZmVyZW5jZXMvRkVUXzAwMF9DT1JFL1JFQURNRS5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICI1ZmVjM2E1MTA3MWE0ZTAzZmZjZWUxYzQzMjBkODE4OTkwZWY1YWQ4N2U1ZDU5YTNjNTZjMDFjMjMyNzVkMzEyIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvc2ltcmVhZHktY29uZm9ybS1wcm9maWxlL3JlZmVyZW5jZXMvRkVUXzAwMF9DT1JFL3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiZGlnZXN0IjogImFlYjdkYmVlNDMxYTY1NGNhYjhkZDMwNzI4NjRjODVkOGNlZGUyODhlMDdiYzMzZWZiMDM5MmRhM2EyNWZiODUiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9zaW1yZWFkeS1jb25mb3JtLXByb2ZpbGUvcmVmZXJlbmNlcy9GRVRfMDAwX0NPUkUvc2NyaXB0cy9yZXBvcnRfc2NoZW1hLmpzb24iLAogICAgICAgICJkaWdlc3QiOiAiYTE1OTJiMDZkNGFkYjY0N2UzZGRkMDAwZDIwYzM4M2VmZTAyNGZlZjlkNTkxMmFjMGIzOGMzZTM4NTc1Yzg3OCIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3NpbXJlYWR5LWNvbmZvcm0tcHJvZmlsZS9yZWZlcmVuY2VzL0ZFVF8wMDBfQ09SRS9zY3JpcHRzL3J1bi5weSIsCiAgICAgICAgImRpZ2VzdCI6ICIxODFhYTE2MzkzZDc1NDFjMDIxNDljZjUxZWFkNDk5MzkyYmFlNTE5YzhjOWEwYjRjY2UxNGIwOWY5NDhmNTk3IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvc2ltcmVhZHktY29uZm9ybS1wcm9maWxlL3JlZmVyZW5jZXMvRkVUXzAwMV9NSU5JTUFML1JFQURNRS5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICIzODI5OTcyMTIxZmMxZTIyZjQxMWQzZmJlNjk4YjVhZjFlMTI5M2QzMDFjNjliZTdkYTMyNDVmMjkwMDE1NjhkIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvc2ltcmVhZHktY29uZm9ybS1wcm9maWxlL3JlZmVyZW5jZXMvRkVUXzAwMV9NSU5JTUFML3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiZGlnZXN0IjogImZlYTg0YjlhMGFlNzJjM2QxZDgzMjM3MTdiODhjOTEyZTE1OTIxZTZlZjFjYWViOWEzODU5MDI3ODE0YzBlNzMiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9zaW1yZWFkeS1jb25mb3JtLXByb2ZpbGUvcmVmZXJlbmNlcy9GRVRfMDAxX01JTklNQUwvc2NyaXB0cy9yZXBvcnRfc2NoZW1hLmpzb24iLAogICAgICAgICJkaWdlc3QiOiAiZmU4ODc4MDZiNTkyMTAxNzg3ZTBhMDAyNTMzMzM2NzliM2FiNmI1ZWQwZTRlYzc5YzRjZDEwY2MyODRhN2Q0YyIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3NpbXJlYWR5LWNvbmZvcm0tcHJvZmlsZS9yZWZlcmVuY2VzL0ZFVF8wMDFfTUlOSU1BTC9zY3JpcHRzL3J1bi5weSIsCiAgICAgICAgImRpZ2VzdCI6ICIxNDkwMDM4Y2Q0Y2MwNTdlZTUxYmZlZWY4M2M5NzRkYTk5OTFiODM1MWQzZTYwYmRmNjhkNjIwZGYyNTdlMzM5IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvc2ltcmVhZHktY29uZm9ybS1wcm9maWxlL3JlZmVyZW5jZXMvRkVUXzAwNF9TSU1VTEFURV9NVUxUSV9CT0RZX1BIWVNJQ1MvUkVBRE1FLm1kIiwKICAgICAgICAiZGlnZXN0IjogIjE2NmEzNjljYTU5M2I1OGIxN2E1Y2RlY2RhNWY4OGIwMGM5N2IzZWQzYzFiOTBhZjIxMTc4NmE4YTJhMDcyODgiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9zaW1yZWFkeS1jb25mb3JtLXByb2ZpbGUvcmVmZXJlbmNlcy9GRVRfMDA0X1NJTVVMQVRFX01VTFRJX0JPRFlfUEhZU0lDUy9zY3JpcHRzL2NoZWNrX2RlcGVuZGVuY2llcy5weSIsCiAgICAgICAgImRpZ2VzdCI6ICI5M2JiYTBiOGNhMzM3NDE5OTYwYzY2ODFmMDg2MGFmZjA1M2RjNWYzZjIzZGM1NmYxODJlZGQwZWNhNzBjZDkwIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvc2ltcmVhZHktY29uZm9ybS1wcm9maWxlL3JlZmVyZW5jZXMvRkVUXzAwNF9TSU1VTEFURV9NVUxUSV9CT0RZX1BIWVNJQ1Mvc2NyaXB0cy9yZXBvcnRfc2NoZW1hLmpzb24iLAogICAgICAgICJkaWdlc3QiOiAiYjQ1Mzk5NWNhMTBlYWEzNTljOTQ5ZDYzZDljNzlkY2ZlOTU4N2Q5NDY5NGMxOWYyN2FmYTllYjA0NmI4MTY0YyIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3NpbXJlYWR5LWNvbmZvcm0tcHJvZmlsZS9yZWZlcmVuY2VzL0ZFVF8wMDRfU0lNVUxBVEVfTVVMVElfQk9EWV9QSFlTSUNTL3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiZGlnZXN0IjogImZhNGY4NjgwNjAwZTlhYjE2MWMyNTcxNDE5YWMxNjE5ZmZlYmFjMzIyZTlkZjU5YjJiNjA0ZWI5NDg0ZDE5NzEiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9zaW1yZWFkeS1jb25mb3JtLXByb2ZpbGUvcmVmZXJlbmNlcy9GRVRfMDA1X1NJTVVMQVRFX0dSQVNQX1BIWVNJQ1MvUkVBRE1FLm1kIiwKICAgICAgICAiZGlnZXN0IjogIjQwZjQzZGQ2Y2RmMWU1MzkyMmEzNDUxZjc0ZDM4YjZlNjdhZGYyNmRlMDAxODk2ZTNjNzg5NDIzMmY5MTNlMTAiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9zaW1yZWFkeS1jb25mb3JtLXByb2ZpbGUvcmVmZXJlbmNlcy9GRVRfMDA1X1NJTVVMQVRFX0dSQVNQX1BIWVNJQ1Mvc2NyaXB0cy9hdXRob3JfZ3Jhc3BfbGluZS5weSIsCiAgICAgICAgImRpZ2VzdCI6ICI4ZTA3MDJmZGYzY2VhZjAyMjg5NTdmY2U5NDg4OGMxMGMwZTcwYjlmMzNkM2RhYjg3Y2UxOWE2MWU0NzliYTU3IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvc2ltcmVhZHktY29uZm9ybS1wcm9maWxlL3NjcmlwdHMvY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiZGlnZXN0IjogIjFlOTY2MWE1MjNlZmJhNTczMzBlYmRiY2NiZmU1NmQ0NTM2Y2EyNzcyY2FkZGU0ZDJmNTE1OTNlMGNlZGNkMzMiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9zaW1yZWFkeS1jb25mb3JtLXByb2ZpbGUvc2NyaXB0cy9yZXBvcnRfc2NoZW1hLmpzb24iLAogICAgICAgICJkaWdlc3QiOiAiMTNjYWZmYzQxNzkwZTJmODk2ODRmNDI1NmQ0MmIzOTc3MzgzNTgxN2RmZGZmYjg5ZDZiOTQ5YmIwZTBkMzQ1NiIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3NpbXJlYWR5LWNvbmZvcm0tcHJvZmlsZS9zY3JpcHRzL3J1bi5weSIsCiAgICAgICAgImRpZ2VzdCI6ICI0ODQ3MmMyNjllOGFmNmM1NDAxOTYxYzU4OWQzZTkzOWI4NjE0NDQxMzFjZTA0YWUxYTkxMDBiYjExOTFiODMzIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvc2ltcmVhZHktdmFsaWRhdGUvUkVBRE1FLm1kIiwKICAgICAgICAiZGlnZXN0IjogIjA3ZGVhMmQ1ZjExMmU4ZDJkZWI5YWU3YmU1MjNiOGIxZjkxNzM2MTY2MTljZDYwZTM5MjBhMmVjZTcwODliZjYiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy9zaW1yZWFkeS12YWxpZGF0ZS9zY3JpcHRzL2NoZWNrX2RlcGVuZGVuY2llcy5weSIsCiAgICAgICAgImRpZ2VzdCI6ICI1NTFhMjk1YTE4ZWU3ZTdlZDgwODAzZDg2MjU1YzI3ZjhlYTU3OWZmMWMwYTk3MTZmNDZlYTJkOGY2MzlhNmFlIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvc2ltcmVhZHktdmFsaWRhdGUvc2NyaXB0cy9yZXBvcnRfc2NoZW1hLmpzb24iLAogICAgICAgICJkaWdlc3QiOiAiYTU2YmYzOTBhMTQ2N2VhNTMwYTBmYjA5YTJiNDAxY2Q2MDQ4ZmFjYzg4MTgxZTExMTM0MTQ1YzhkY2ZjNTI0NiIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3NpbXJlYWR5LXZhbGlkYXRlL3NjcmlwdHMvcnVuLnB5IiwKICAgICAgICAiZGlnZXN0IjogImJlZGU4MjgxMGEzYWZlYmYyM2JhNTRjZTYzOGY4ZjNjYjUyMmRmMWMxNGYwMmNjYjQ4MmNhNmU2MWUxZjI1MTAiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy90cm91Ymxlc2hvb3RpbmcubWQiLAogICAgICAgICJkaWdlc3QiOiAiNTA1ZmUzMmM2ODZkY2M5MmVlMzhlMjc0NWRkNTk3NTI0NzgyN2E4NzljNDIwZDk0YzJkNmFhMzNlYzQzNzM1OSIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3ZhbGlkYXRlLXVzZC1taW5pbXVtL1JFQURNRS5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICI0YmJjODY5OTY0ZjJiNjY0ODU2MDNkNTkwOWUwNTRhNmM3ZTE4YmRiN2IyM2ZjODk3MTFmZGM1NjVlMjZiY2EyIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvdmFsaWRhdGUtdXNkLW1pbmltdW0vc2NyaXB0cy9jaGVja19kZXBlbmRlbmNpZXMucHkiLAogICAgICAgICJkaWdlc3QiOiAiZmZmMjg2NWVhOGM1MDMyZDY4NTNhOTdkY2ViNGY4NGQ5YzRhZWIzYTU1MjhkMzBlYmI1MjNhZmRjMGNlNzk0NiIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJyZWZlcmVuY2VzL3ZhbGlkYXRlLXVzZC1taW5pbXVtL3NjcmlwdHMvcmVwb3J0X3NjaGVtYS5qc29uIiwKICAgICAgICAiZGlnZXN0IjogIjg2OWZkNjk2YTllYmMyZWY4MmYxY2M3MTFjMjMxYTU1MzdkMzY1MjVkOTNlOTdlYjBjOWFlYTRiYWFmZGYxYTUiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAicmVmZXJlbmNlcy92YWxpZGF0ZS11c2QtbWluaW11bS9zY3JpcHRzL3J1bi5weSIsCiAgICAgICAgImRpZ2VzdCI6ICIyNzgxNGU2MGZlZTg3MzIxMjc0NWY4OWNmZTM3MDM5NjRkYzYyYTZmNzMwMjQ3YjhjNWQ3ZTY4NjIyNGNkNDNmIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInJlZmVyZW5jZXMvd29ya2Zsb3cubWQiLAogICAgICAgICJkaWdlc3QiOiAiMThiN2RmNzAxYWRkYjc5OGVjNmZkNDZlNjhkNDhhNTMwMWQxZmQ1NjkzNjZmMmVjNzk4NWI4ZTRjOTgyMTdkZCIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAibmFtZSI6ICJzaGFyZWQvcHJlZmxpZ2h0X21hbmlmZXN0LnB5IiwKICAgICAgICAiZGlnZXN0IjogIjVkMWE1NGIwNThkOWZhYjJiYTUyOWQwNTlmZTA4ZDExMWFhMDE0NjcyYzczMTkwZmE2ODI1ZGFlNDlmMDllMDkiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAic2hhcmVkL3NjcmlwdF91dGlscy5weSIsCiAgICAgICAgImRpZ2VzdCI6ICI5NzJhMWIwMWYwYTc5OTkxYmZmYjgxMWJkMjAyOGY3Zjk4Yzg3YTAxYTMyZGI1MWU1NjY3ZGFlMzc5MmFlZjcyIgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInNoYXJlZC9zaW1yZWFkeV9wYWNrYWdlLnB5IiwKICAgICAgICAiZGlnZXN0IjogImYyNDU3MjcwNGYwZTc3NTMzNzc0NDMzMDRmMmY5NzVkYWIzYjFiM2UzMjVmZWFmOTE2MjM1NGFmODRmZGFjY2QiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAic2hhcmVkL3NpbXJlYWR5X3BhY2thZ2VfY2hlY2tfZGVwZW5kZW5jaWVzLnB5IiwKICAgICAgICAiZGlnZXN0IjogIjIwZmEzMGU5YmYxZGU0YTc2MzE1ZDU1NzNhMjUzYjk3N2U4ODViNzgxZGQwYmQzMTNiYzBlN2ZjZDJjYmIyMWMiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAic2hhcmVkL3NpbXJlYWR5X3BhY2thZ2VfcmVwb3J0X3NjaGVtYS5qc29uIiwKICAgICAgICAiZGlnZXN0IjogIjc3OWE1ZjZiMTM5MGY1MTI1NDBkNTNmMTMxNjdjZjVjZDAyNzExZWRjMDAzYWJmZjg5YzUzMmMwNjJkZWJmMDciCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAic2hhcmVkL3Vwc3RyZWFtX3ZlcnNpb25zLnB5IiwKICAgICAgICAiZGlnZXN0IjogImFkMjU1Y2I4ZWNkMjMxY2FiYWVkNzBiMTNhNjhkNDRhZTc1ZTllNWQ0MDU4MDRkOWEwZWNhNDQ3NWVmN2RlOWIiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgIm5hbWUiOiAic2tpbGwtY2FyZC5tZCIsCiAgICAgICAgImRpZ2VzdCI6ICI0NWJjOWQ5ODA1Njg5OTEzMjE2ZWQ4ODM5ODkyOGQ4MDNlNzIzNmQxOGE1MGIxM2FmZGVmZTZhYTQyYTIzMTg1IgogICAgICB9LAogICAgICB7CiAgICAgICAgImFsZ29yaXRobSI6ICJzaGEyNTYiLAogICAgICAgICJuYW1lIjogInVwc3RyZWFtLXZlcnNpb25zLmxvY2suanNvbiIsCiAgICAgICAgImRpZ2VzdCI6ICI0YzVlNjM1MmRiNzg2ZmM3NDRkOTE0ZDM5OGIyZjdlNjNiOWViOWRmZmVhMWNhYzViY2ViMGU1NzJjYjNhOTI0IgogICAgICB9CiAgICBdLAogICAgInNlcmlhbGl6YXRpb24iOiB7CiAgICAgICJhbGxvd19zeW1saW5rcyI6IGZhbHNlLAogICAgICAibWV0aG9kIjogImZpbGVzIiwKICAgICAgImlnbm9yZV9wYXRocyI6IFsKICAgICAgICAiLmdpdGlnbm9yZSIsCiAgICAgICAgIi5naXQiLAogICAgICAgICIuZ2l0aHViIiwKICAgICAgICAiLmdpdGF0dHJpYnV0ZXMiCiAgICAgIF0sCiAgICAgICJoYXNoX3R5cGUiOiAic2hhMjU2IgogICAgfQogIH0KfQ==","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MGYCMQDNswlgMZQvnrbmghtAA+SvI/1aHvjuBQkDPANrjteYm4wv5yOjHZ4bAS5pXvTedYUCMQDIC6joLIH9eyroU5b0vI0w4VEuYECmFXxuuI4BH/L7kgnJPXa0dEUMsodAdv+XXCQ=","keyid":""}]}} \ No newline at end of file diff --git a/skills/omniverse-cad-to-simready/upstream-versions.lock.json b/skills/omniverse-cad-to-simready/upstream-versions.lock.json new file mode 100644 index 00000000..08376cf6 --- /dev/null +++ b/skills/omniverse-cad-to-simready/upstream-versions.lock.json @@ -0,0 +1,81 @@ +{ + "schema_version": "1.0", + "tested_integration": { + "branch": "byungsook/qa-rerun-20260803", + "commit": "6b659812db791354da24de4583cd8cc9339443b7", + "validated_at": "2026-08-04" + }, + "git_upstreams": { + "content_agents": { + "checkout": "content-agents", + "commit": "36dbf3f274f8e256637230a05a085853f65cc175", + "env": "CONTENT_AGENTS_UPSTREAM_ROOT", + "ref": "v0.5.2", + "url": "https://github.com/nvidia-omniverse/content-agents" + }, + "simready_foundation": { + "checkout": "simready-foundation", + "commit": "a1e9dd68ee2d107f74dc6cd6da875b54ad3f8fd3", + "env": "SIMREADY_FOUNDATION_ROOT", + "ref": "v2026.04.1", + "url": "https://github.com/NVIDIA/simready-foundation" + }, + "usd_convert_cad": { + "checkout": "usd-convert-cad", + "commit": "208fe2c1cd71ae2bb7bd825daf712617000ae028", + "env": "USD_CONVERT_CAD_ROOT", + "ref": "v0.2.0", + "url": "https://github.com/NVIDIA-Omniverse/usd-convert-cad" + }, + "usd_convert_gsplat": { + "checkout": "usd-convert-gsplat", + "commit": "621017ebf78394488260c70ec4eadd70ff621131", + "env": "USD_CONVERT_GSPLAT_ROOT", + "ref": "v0.1.15", + "url": "https://github.com/NVIDIA-Omniverse/usd-convert-gsplat" + } + }, + "python_packages": { + "jinja2": "3.1.6", + "markdown-it-py": "4.2.0", + "markupsafe": "3.0.3", + "mdurl": "0.1.2", + "numpy": "1.26.4", + "omniverse-asset-validator": "1.18.0", + "omniverse-usd-profiles": "1.10.22", + "simready-validate": "2026.4.8", + "usd-convert-cad": "0.2.0", + "usd-exchange": "2.3.0" + }, + "python_sources": { + "gsplat2usd": { + "commit": "c965f84bcbdc9667bb9631c6c922d12b4fea20ce", + "subdirectory": "source/python", + "url": "https://github.com/NVIDIA-Omniverse/gsplat-converter.git", + "version": "0.1.5" + } + }, + "runtime_sets": { + "simready_validate": { + "no_deps": [ + "simready-validate" + ], + "packages": [ + "usd-exchange", + "omniverse-asset-validator", + "omniverse-usd-profiles", + "numpy", + "jinja2", + "markdown-it-py", + "markupsafe", + "mdurl" + ] + }, + "usd_convert_cad": { + "no_deps": [], + "packages": [ + "usd-convert-cad" + ] + } + } +}