Skip to content

fix(skills): harden UAT debug-bundle triage against malformed input#1914

Open
njhensley wants to merge 1 commit into
NVIDIA:mainfrom
njhensley:fix/uat-report-triage-robustness
Open

fix(skills): harden UAT debug-bundle triage against malformed input#1914
njhensley wants to merge 1 commit into
NVIDIA:mainfrom
njhensley:fix/uat-report-triage-robustness

Conversation

@njhensley

Copy link
Copy Markdown
Member

Summary

Addresses the three CodeRabbit findings on #1913: a crash in the triage digest, an unsanitized download path, and a directory collision when a run carries more than one debug bundle.

Motivation / Context

All three were raised on #1913 after it merged. The first is a genuine defect that defeats the feature's own error-handling contract; the other two are latent.

Fixes: N/A
Related: #1913

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • Build/CI/tooling

Component(s) Affected

  • CLI (cmd/aicr, pkg/cli)
  • API server (cmd/aicrd, pkg/server)
  • Recipe engine / data (pkg/recipe)
  • Bundlers (pkg/bundler, pkg/component/*)
  • Collectors / snapshotter (pkg/collector, pkg/snapshotter)
  • Validator (pkg/validator)
  • Core libraries (pkg/errors, pkg/k8s)
  • Docs/examples (docs/, examples/)
  • Other: agent skills (.agents/skills/aicr-uat-report/)

Implementation Notes

1. TypeError aborted the download loop (real defect). report.json with an explicit "tests": null made .get("tests", []) return None — a default only applies when the key is absent — so iteration raised TypeError, which the except (OSError, ValueError, AttributeError) clause did not list. triage_digest is called per run with no surrounding guard, so one truncated report killed every remaining run. That directly contradicts the contract #1913 introduced, where a bad artifact is reported per run and processing continues.

Reproduced against the merged code before fixing:

PRE-FIX CRASH CONFIRMED: TypeError: 'NoneType' object is not iterable

Fixed with or fallbacks for null-valued keys plus TypeError in the except tuple. A malformed report now degrades to report.json present but unparseable for that one run.

2. Unsanitized path components. Reservation tokens reach the script via the workflow run-name, and NEW_TITLE/OLD_TITLE accept any non-whitespace token, so svc/gpu are not guaranteed well-formed before being joined onto the output directory. New slug() collapses everything outside [a-z0-9-], which removes separators and dot runs together. Reaching this requires write access to dispatch the workflow, so it is defense in depth rather than a live exposure — but it is one line and the destination is attacker-influenced in principle.

3. Multi-artifact collision. A run carrying more than one debug bundle unpacked them into a shared directory, merging two clusters' state so the printed digest described neither. Each artifact now gets its own subdirectory once there is more than one; the single-bundle path (every run observed so far) is unchanged.

Also corrected the debug_artifacts docstring, which claimed to filter expired artifacts and sort newest-first. It does neither — the caller filters deliberately, so that "never uploaded" and "aged out" stay distinguishable.

Testing

python3 .agents/skills/aicr-uat-report/uat_report.py --days 2 \
  --download-debug /tmp/uat-debug2 --max-downloads 2

Live regression run: green, output unchanged from #1913 for the normal path (one bundle fetched, one "no artifact" correctly reported).

Targeted cases, all exercised directly against triage_digest and slug:

Input Result
{"results": {"tests": null}} 0/0 checks failing (crashed pre-fix)
{"results": null} 0/0 checks failing
[1,2,3] (top-level list) present but unparseable
truncated JSON present but unparseable
valid report 1/1 checks failing: x(failed)
slug("EKS","GB200","training",id) eks-gb200-training-<id> — unchanged
slug("../../etc","H100",…) etc-h100-…, stays under the output dir
slug("AWS/../..","..",…) aws----inference-2, stays under the output dir
slug("...","","","") unknown

No exception escaped in any case; every traversal attempt resolved inside the download directory.

make qualify not run — no Go, YAML, or build inputs changed; verified with py_compile plus the runs above.

Risk Assessment

  • Low — Isolated change, well-tested, easy to revert
  • Medium — Touches multiple components or has broader impact
  • High — Breaking change, affects critical paths, or complex rollout

Rollout notes: Agent tooling only. Behaviour on well-formed input is identical to #1913; the changes only affect malformed-input and multi-artifact paths.

Checklist

  • Tests pass locally (make test with -race) — N/A, no Go changes; see Testing
  • Linter passes (make lint) — N/A, no Go/YAML changes; py_compile clean
  • I did not skip/disable tests to make CI green
  • I added/updated tests for new functionality — N/A, skills have no test harness; cases exercised directly, table above
  • I updated docs if user-facing behavior changed — no user-visible behaviour change
  • Changes follow existing patterns in the codebase
  • Commits are cryptographically signed (git commit -S)

Review follow-ups from NVIDIA#1913.

triage_digest crashed the whole --download-debug loop on a report.json
carrying an explicit "tests": null — .get("tests", []) returns None only
when the key is absent, so iteration raised TypeError, which the except
clause did not list. Confirmed against the merged code. Use `or` fallbacks
for null-valued keys and catch TypeError, so one truncated report degrades
to "unparseable" for that run instead of aborting the remaining runs.

Sanitize the download directory name. Reservation tokens reach the script
through the workflow run-name and the title regexes accept any
non-whitespace token, so svc/gpu are not guaranteed well-formed; collapse
everything outside [a-z0-9-] to strip separators and dot runs before
joining onto the output directory. Requires repo write access to reach,
so this is defense in depth rather than a live exposure.

Give each artifact its own subdirectory when a run carries more than one
debug bundle — a shared directory merged two clusters' state and left the
digest describing neither. Also correct the debug_artifacts docstring: it
returns expired artifacts too, and never sorted.

Signed-off-by: Nathan Hensley <nhensley@nvidia.com>
@njhensley
njhensley requested a review from a team as a code owner July 24, 2026 20:42
@njhensley njhensley self-assigned this Jul 24, 2026
@njhensley njhensley added the theme/ci-dx CI pipelines, developer experience, and build tooling label Jul 24, 2026
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The UAT report workflow now includes expired debug artifacts in listings and adds filesystem-safe directory naming. Multiple active artifacts for a run are downloaded into separate directories. Triage parsing treats null results data as empty and catches TypeError for malformed reports. Digest generation and output now use each artifact’s dedicated directory.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: mchmarny

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: hardening UAT debug-bundle triage against malformed input.
Description check ✅ Passed The description accurately summarizes the fixes and is clearly related to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In @.agents/skills/aicr-uat-report/uat_report.py:
- Around line 123-133: Add type annotations to the variadic parts parameter and
return value of slug, using types compatible with the existing str conversion
and filesystem-safe string result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 3bb32642-afaa-4520-9036-4616dfebc708

📥 Commits

Reviewing files that changed from the base of the PR and between 7e0f4a1 and 8dc352b.

📒 Files selected for processing (1)
  • .agents/skills/aicr-uat-report/uat_report.py

Comment on lines +123 to +133
def slug(*parts):
"""Build a filesystem-safe directory name from run metadata.

Reservation names reach us through the workflow's run-name, and the title
regexes accept any non-whitespace token, so `svc`/`gpu` are not guaranteed
to be well-formed. Collapsing everything outside [a-z0-9-] removes both
separators and dot runs, so no component can traverse out of the download
directory.
"""
raw = "-".join(str(p) for p in parts).lower()
return re.sub(r"[^a-z0-9-]+", "-", raw).strip("-") or "unknown"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add annotations to slug.

Ruff reports ANN002 for the untyped variadic parameter. Add parameter and return annotations.

Suggested fix
-def slug(*parts):
+def slug(*parts: object) -> str:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def slug(*parts):
"""Build a filesystem-safe directory name from run metadata.
Reservation names reach us through the workflow's run-name, and the title
regexes accept any non-whitespace token, so `svc`/`gpu` are not guaranteed
to be well-formed. Collapsing everything outside [a-z0-9-] removes both
separators and dot runs, so no component can traverse out of the download
directory.
"""
raw = "-".join(str(p) for p in parts).lower()
return re.sub(r"[^a-z0-9-]+", "-", raw).strip("-") or "unknown"
def slug(*parts: object) -> str:
"""Build a filesystem-safe directory name from run metadata.
Reservation names reach us through the workflow's run-name, and the title
regexes accept any non-whitespace token, so `svc`/`gpu` are not guaranteed
to be well-formed. Collapsing everything outside [a-z0-9-] removes both
separators and dot runs, so no component can traverse out of the download
directory.
"""
raw = "-".join(str(p) for p in parts).lower()
return re.sub(r"[^a-z0-9-]+", "-", raw).strip("-") or "unknown"
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 123-123: Missing type annotation for *parts

(ANN002)

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

In @.agents/skills/aicr-uat-report/uat_report.py around lines 123 - 133, Add
type annotations to the variadic parts parameter and return value of slug, using
types compatible with the existing str conversion and filesystem-safe string
result.

Source: Linters/SAST tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/S theme/ci-dx CI pipelines, developer experience, and build tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant