feat(evidence): per-source pointer contract + signer allowlist (#1401)#1485
Conversation
…A#1401) Establish the in-repo recipes/evidence/ pointer contract per NVIDIA#1347 (Option A): one immutable single-attestation pointer per run at recipes/evidence/<recipe>/<src>/<bundle-digest>.yaml, where <src> is a one-way slug derived from the verified signer's OIDC identity. Two sources for one recipe no longer collide, and the path is not squattable. - Add attestation.SourceSlug(issuer, identity) = sha256(issuer\nidentity) first 12 hex; aligns with GP2 ingest keying. - Migrate the two flat pointers into the per-source layout. - Add pkg/evidence/allowlist + recipes/evidence/allowlist.yaml as the maintained trust root: first-party / community / partner classes, disjoint and non-overlapping. Community/partner entries are slug-only (source + issuer + optional non-PII label) so no cleartext contributor identity is committed; the loader rejects a stray identity field. First-party entries pin tightly-bounded workflow identityPatterns. - Add verifier.DiscoverPointers (glob multi-source discovery) and verifier.CheckEvidenceTree (anti-squat: signed, signer hashes to its own dir, allowlisted community/partner). - Enforce in CI: tools/evidence-pointercheck + evidence-pointer-contract workflow (offline, fork-safe); CODEOWNERS makes maintainer review the trust gate. - Generalize the warning-only recipe-evidence-check.sh and the emit/publish copyTo hint to the per-source layout; update CLI help and docs (artifact-verification, validation, maintaining, recipe-development, cli-reference, ADR-007). First-party UAT ingests evidence directly (no per-run commit); committed pointers are the community/partner channel. Out of scope: GP2 ingest, GP3 bucket/SA, GP4 generator.
7abf277 to
64c9bac
Compare
Recipe evidence checkProtected recipesRecipes with committed evidence (
How to refresh evidenceRun on a cluster matching the recipe's aicr snapshot -o snapshot.yaml
aicr validate \
-r recipes/overlays/<slug>.yaml \
-s snapshot.yaml \
--emit-attestation ./out \
--push ghcr.io/<your-fork>/aicr-evidence
# Copy to the per-source path printed in the emit 'copyTo' hint:
# recipes/evidence/<slug>/<source>/<bundle-digest>.yamlThis gate is warning-only and never blocks merge. See ADR-007 for the trust model. |
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Enterprise Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR changes the evidence pointer contract to a per-source layout under Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 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 @.github/scripts/recipe-evidence-check.sh:
- Around line 240-241: The report table in recipe-evidence-check.sh needs a
pointer identifier so repeated rows from the same source can be distinguished.
Update the row rendering in the main report loop (and any related aggregation
helpers around the table output) to include the pointer basename or signed
digest as its own column, or otherwise collapse duplicates before printing. Make
sure the header and each emitted row in the recipe/source/verify output stay
aligned with the new pointer-specific field.
In `@docs/contributor/maintaining.md`:
- Around line 90-92: The pointer-file requirement is too vague and should
explicitly name the immutable digest-pinned file used for each run. Update the
maintainer guidance to reference the per-source pointer file under
recipes/evidence/<recipe>/<src>/ by its bundle-digest filename pattern, and make
sure the wording in this section clearly ties the contract to the
one-file-per-run pointer expected by the CI gate.
In `@docs/design/007-recipe-evidence.md`:
- Around line 682-685: Add a language tag to the fenced block in the recipe
evidence design doc so markdownlint stops flagging it; update the fence around
the path listing in the relevant section of the documentation to use a text
language identifier, keeping the existing content unchanged.
In `@pkg/evidence/allowlist/allowlist.go`:
- Around line 191-204: `validateEntry` currently only checks that exactly one of
`Source` or `IdentityPattern` is set, but it does not enforce the per-class
rules. Update `validateEntry` in allowlist to reject `IdentityPattern` unless
`class` is first-party, and reject `Source` when `class` is first-party, while
keeping the existing issuer and exclusivity checks. Add negative tests covering
a community/partner entry with `identityPattern` and a first-party entry with
`source` so `Load` fails in both cases.
In `@pkg/evidence/attestation/source.go`:
- Around line 24-28: Increase the source slug width used by source ownership and
allowlist matching, since SourceSlugLength is currently too small for an
authorization key. Update the SourceSlugLength constant in source.go to a
materially larger value (at least 128 bits or the full digest), and verify any
code that depends on this slug, especially Allowlist.Classify and
verifier.checkPointerFile, continues to use the same slug length consistently
for directory ownership and trust-root classification.
In `@pkg/evidence/verifier/discover.go`:
- Around line 81-84: The error handling in discover should preserve the
structured code returned by allowlist.Load instead of always re-wrapping it as
ErrCodeInvalidRequest. Update the allowlist.Load call site in discover.go to use
errors.PropagateOrWrap so existing pkg/errors codes like NotFound or Internal
are passed through unchanged, while still adding the local context message for
non-coded errors. Use the discover logic around allowlist.Load and the existing
errors package pattern as the reference point.
- Around line 175-179: The authorization check in checkPointerFile currently
trusts any allowlist match from al.Classify, but slug-only matches can bypass
issuer validation. Update the check around signer.Issuer and signer.Identity to
require that the returned allowlist entry’s issuer matches signer.Issuer before
accepting it, or adjust allowlist.Classify so the slug fast-path also validates
issuer. Use the existing symbols checkPointerFile, al.Classify, and the
allowlist logic in allowlist.go to locate and fix the flow.
🪄 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: b6f7ec8a-3e85-40c6-8052-628237d98eea
📒 Files selected for processing (26)
.github/CODEOWNERS.github/scripts/recipe-evidence-check.sh.github/workflows/evidence-pointer-contract.yamldocs/contributor/maintaining.mddocs/design/007-recipe-evidence.mddocs/integrator/recipe-development.mddocs/user/artifact-verification.mddocs/user/cli-reference.mddocs/user/validation.mdpkg/cli/evidence_digest.gopkg/cli/evidence_verify.gopkg/cli/validate.gopkg/evidence/allowlist/allowlist.gopkg/evidence/allowlist/allowlist_test.gopkg/evidence/attestation/emit.gopkg/evidence/attestation/pointer.gopkg/evidence/attestation/pointer_test.gopkg/evidence/attestation/publish.gopkg/evidence/attestation/source.gopkg/evidence/attestation/source_test.gopkg/evidence/verifier/discover.gopkg/evidence/verifier/discover_test.gorecipes/evidence/allowlist.yamlrecipes/evidence/gb200-eks-ubuntu-training/7c4c0edc8c76/sha256-8274b6a1da24aa9782dc12162bf6a38265c30a852585ca64cfad5718efbbdec3.yamlrecipes/evidence/h100-gke-cos-training/7c4c0edc8c76/sha256-33d4cf36622ead990c43a596f6f53c62b87d9fa4708f59b7e3f356f215e54317.yamltools/evidence-pointercheck/main.go
There was a problem hiding this comment.
Actionable comments posted: 11
♻️ Duplicate comments (3)
docs/design/007-recipe-evidence.md (1)
385-394: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winName the digest-pinned file here.
This bullet still points to
recipes/evidence/<slug>.yaml, which is not the per-source contract described elsewhere in this ADR. Spell out the immutable filename so the protected-set wording matches the CI/verifier path shape.Suggested fix
- iff it has a committed pointer at `recipes/evidence/<slug>.yaml`. + iff it has a committed pointer file at + `recipes/evidence/<recipe>/<src>/<bundle-digest>.yaml`.🤖 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 `@docs/design/007-recipe-evidence.md` around lines 385 - 394, The “Protected vs. other affected” bullet still refers to the generic recipes/evidence/<slug>.yaml pointer, but it should name the immutable digest-pinned filename used by the CI/verifier contract. Update the wording in this section to explicitly call out the per-source pinned file path so the protected-set description matches the rest of the ADR and the verifier path shape.docs/contributor/maintaining.md (1)
90-92: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winSpell out the digest-pinned filename.
recipes/evidence/<recipe>/<src>/is still too vague here. The checklist should name the immutable<bundle-digest>.yamlfile so it matches the CI gate and the ADR contract.Suggested fix
-1. **Pointer file present.** At least one per-source pointer under - `recipes/evidence/<recipe>/<src>/` exists for every touched overlay. +1. **Pointer file present.** At least one per-source pointer file under + `recipes/evidence/<recipe>/<src>/<bundle-digest>.yaml` exists for every touched overlay.🤖 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 `@docs/contributor/maintaining.md` around lines 90 - 92, The pointer-file checklist is too vague and should explicitly name the immutable digest-pinned artifact. Update the “Pointer file present” item in the maintaining guide to reference the exact `<bundle-digest>.yaml` filename under `recipes/evidence/<recipe>/<src>/`, matching the CI gate and ADR contract. Keep the wording aligned with the existing checklist entry so it clearly states that each touched overlay must have the corresponding digest-pinned pointer file.docs/integrator/recipe-development.md (1)
658-658: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winSpell out the digest-pinned filename.
recipes/evidence/<recipe>/<src>/is still too vague here. Name the immutable<bundle-digest>.yamlfile so maintainers and the CI gate are talking about the same thing.Suggested fix
- per-source pointer under `recipes/evidence/<recipe>/<src>/`. + per-source pointer file under + `recipes/evidence/<recipe>/<src>/<bundle-digest>.yaml`.🤖 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 `@docs/integrator/recipe-development.md` at line 658, The reference to the per-source pointer under recipes/evidence/<recipe>/<src>/ is too vague; update the documentation in this section to explicitly name the immutable digest-pinned <bundle-digest>.yaml file so readers and CI are referring to the same artifact. Use the existing recipe/evidence path wording around the pointer location, but replace the generic folder-only reference with the exact filename convention.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/scripts/recipe-evidence-check.sh:
- Around line 545-559: The digest status logic in recipe-evidence-check.sh is
using signed_digest even when verification has already failed, which can produce
misleading matches/stale output and extra warnings. Update the digest rendering
block that builds digest_cell so it only compares signed_digest against
current_digest when verify_ok is true, and otherwise emit a
verification-failed/ignored digest state without doing the comparison. Keep the
existing warning counting behavior aligned with the verify_ok branch so a failed
verify does not add a second digest-related warning.
- Around line 71-77: The md_escape helper only strips newline characters, so
verifier-derived values can still include carriage returns and break the
generated Markdown table row. Update md_escape to normalize both newline and
carriage return characters before escaping pipes and removing backticks, keeping
the sanitization logic in the md_escape function consistent for all table-cell
inputs.
- Around line 442-446: The digest computation in the `recipe-evidence-check.sh`
loop still runs `aicr evidence digest` for every protected slug even after the
`MAX_ROWS` cap has effectively been reached. Update the row-processing logic
around `current_digest` and the pointer-row truncation path so the loop stops
invoking the digest command once the cap is hit, while still preserving the
summary/report behavior. Use the existing `MAX_ROWS` control flow and the
per-row handling around the `digest_err`/`current_digest` block to short-circuit
further digest work as soon as no more rows can be emitted.
In `@docs/user/cli-reference.md`:
- Around line 2670-2694: The `aicr evidence sign` docs still point users at the
legacy `recipes/evidence/<recipe>.yaml` target, which is no longer correct under
the per-source pointer contract. Update the synopsis and example text in the
`aicr evidence sign` section to describe the new per-source committed pointer
location and keep the wording aligned with the command’s current contract for
the pointer argument.
In `@pkg/evidence/allowlist/allowlist.go`:
- Around line 121-143: Make allowlist loading cancelable by threading a context
with timeout through the allowlist read path. Update Load in allowlist.go to
accept context.Context and use it to guard the blocking open/read work, then
propagate that context from the only caller in discover.go so the operation can
be bounded and canceled. Keep the existing validation and error wrapping
behavior in Load, but ensure the new context-aware flow is followed from
verifier discovery into allowlist.Load.
In `@pkg/evidence/attestation/pointer_test.go`:
- Around line 87-119: Refactor TestPointerCopyToHint into a table-driven test to
cover both independent cases in one pattern. Keep the existing assertions for
PointerCopyToHint, but express the signed path and unsigned guidance scenarios
as separate table entries with names and expected outcomes. Use the existing
Pointer, PointerAttestation, PointerBundle, and PointerSigner setup within each
case so the test follows the repo’s standard _test.go table-driven style.
In `@pkg/evidence/attestation/pointer.go`:
- Around line 112-115: Update the WritePointerFile docstring to describe the new
per-source layout instead of the old committed pointer path. In the comment near
WritePointerFile, replace the `recipes/evidence/<recipe>.yaml` example with
`recipes/evidence/<recipe>/<source>/<digest>.yaml`, and keep the wording aligned
with the actual in-place patching behavior for committed pointers.
- Around line 122-154: The WritePointerFile helper currently ignores caller
cancellation because it has no context parameter and does not check deadlines
during the temp-file create/write/rename flow. Update WritePointerFile in
pointer.go to accept context.Context and thread it through the pointer
persistence path, using the existing MarshalPointer, os.CreateTemp, tmp.Write,
tmp.Close, and os.Rename steps as checkpoints to abort early when ctx is done.
Make sure every filesystem step returns promptly on cancellation and preserves
the current error-wrapping behavior.
In `@pkg/evidence/attestation/publish.go`:
- Around line 49-53: The NoSign publish flow is logging the wrong copy-to hint
because PointerCopyToHint(pointer) treats an empty signer block as “sign and
push,” even though the bundle has already been pushed. Update the publish path
in publish.go (the NoSign branch and any shared hint helper used there) so the
next-step message distinguishes “unsigned but already pushed” from “not yet
pushed,” and avoid telling users to repeat the push; either special-case
opts.NoSign at the call site or extend PointerCopyToHint to accept that state.
In `@pkg/evidence/verifier/discover.go`:
- Around line 155-183: The pointer validation in checkPointerFile is missing the
filename contract check, so a misnamed YAML file can still be accepted under the
correct recipe/source path. Update checkPointerFile to compare
filepath.Base(path) against the bundle digest declared in the loaded pointer
(the committed pointer name should be <bundle-digest>.yaml), and return an error
when they differ; use the existing LoadAndValidatePointer and ptr fields in
discover.go to enforce this alongside the recipe and source checks.
- Around line 141-147: The directory scan under the evidence tree is too
permissive because the loop in discover.go skips subdirectories and non-“.yaml”
entries, allowing unexpected committed children to evade validation. Update the
file iteration logic that builds TreeProblem entries so every unexpected item
under sourcePath is reported as a problem instead of continuing, and only the
allowed add-only <recipe>/<source>/<bundle-digest>.yaml layout passes. Keep the
existing checkPointerFile path for valid YAML files and add explicit rejection
handling for f.IsDir() and non-YAML names in the same discovery flow.
---
Duplicate comments:
In `@docs/contributor/maintaining.md`:
- Around line 90-92: The pointer-file checklist is too vague and should
explicitly name the immutable digest-pinned artifact. Update the “Pointer file
present” item in the maintaining guide to reference the exact
`<bundle-digest>.yaml` filename under `recipes/evidence/<recipe>/<src>/`,
matching the CI gate and ADR contract. Keep the wording aligned with the
existing checklist entry so it clearly states that each touched overlay must
have the corresponding digest-pinned pointer file.
In `@docs/design/007-recipe-evidence.md`:
- Around line 385-394: The “Protected vs. other affected” bullet still refers to
the generic recipes/evidence/<slug>.yaml pointer, but it should name the
immutable digest-pinned filename used by the CI/verifier contract. Update the
wording in this section to explicitly call out the per-source pinned file path
so the protected-set description matches the rest of the ADR and the verifier
path shape.
In `@docs/integrator/recipe-development.md`:
- Line 658: The reference to the per-source pointer under
recipes/evidence/<recipe>/<src>/ is too vague; update the documentation in this
section to explicitly name the immutable digest-pinned <bundle-digest>.yaml file
so readers and CI are referring to the same artifact. Use the existing
recipe/evidence path wording around the pointer location, but replace the
generic folder-only reference with the exact filename convention.
🪄 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: c08aa453-1c4e-42a8-b733-d98e2855c4db
📒 Files selected for processing (26)
.github/CODEOWNERS.github/scripts/recipe-evidence-check.sh.github/workflows/evidence-pointer-contract.yamldocs/contributor/maintaining.mddocs/design/007-recipe-evidence.mddocs/integrator/recipe-development.mddocs/user/artifact-verification.mddocs/user/cli-reference.mddocs/user/validation.mdpkg/cli/evidence_digest.gopkg/cli/evidence_verify.gopkg/cli/validate.gopkg/evidence/allowlist/allowlist.gopkg/evidence/allowlist/allowlist_test.gopkg/evidence/attestation/emit.gopkg/evidence/attestation/pointer.gopkg/evidence/attestation/pointer_test.gopkg/evidence/attestation/publish.gopkg/evidence/attestation/source.gopkg/evidence/attestation/source_test.gopkg/evidence/verifier/discover.gopkg/evidence/verifier/discover_test.gorecipes/evidence/allowlist.yamlrecipes/evidence/gb200-eks-ubuntu-training/7c4c0edc8c76/sha256-8274b6a1da24aa9782dc12162bf6a38265c30a852585ca64cfad5718efbbdec3.yamlrecipes/evidence/h100-gke-cos-training/7c4c0edc8c76/sha256-33d4cf36622ead990c43a596f6f53c62b87d9fa4708f59b7e3f356f215e54317.yamltools/evidence-pointercheck/main.go
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 11
♻️ Duplicate comments (3)
docs/design/007-recipe-evidence.md (1)
385-394: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winName the digest-pinned file here.
This bullet still points to
recipes/evidence/<slug>.yaml, which is not the per-source contract described elsewhere in this ADR. Spell out the immutable filename so the protected-set wording matches the CI/verifier path shape.Suggested fix
- iff it has a committed pointer at `recipes/evidence/<slug>.yaml`. + iff it has a committed pointer file at + `recipes/evidence/<recipe>/<src>/<bundle-digest>.yaml`.🤖 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 `@docs/design/007-recipe-evidence.md` around lines 385 - 394, The “Protected vs. other affected” bullet still refers to the generic recipes/evidence/<slug>.yaml pointer, but it should name the immutable digest-pinned filename used by the CI/verifier contract. Update the wording in this section to explicitly call out the per-source pinned file path so the protected-set description matches the rest of the ADR and the verifier path shape.docs/contributor/maintaining.md (1)
90-92: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winSpell out the digest-pinned filename.
recipes/evidence/<recipe>/<src>/is still too vague here. The checklist should name the immutable<bundle-digest>.yamlfile so it matches the CI gate and the ADR contract.Suggested fix
-1. **Pointer file present.** At least one per-source pointer under - `recipes/evidence/<recipe>/<src>/` exists for every touched overlay. +1. **Pointer file present.** At least one per-source pointer file under + `recipes/evidence/<recipe>/<src>/<bundle-digest>.yaml` exists for every touched overlay.🤖 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 `@docs/contributor/maintaining.md` around lines 90 - 92, The pointer-file checklist is too vague and should explicitly name the immutable digest-pinned artifact. Update the “Pointer file present” item in the maintaining guide to reference the exact `<bundle-digest>.yaml` filename under `recipes/evidence/<recipe>/<src>/`, matching the CI gate and ADR contract. Keep the wording aligned with the existing checklist entry so it clearly states that each touched overlay must have the corresponding digest-pinned pointer file.docs/integrator/recipe-development.md (1)
658-658: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winSpell out the digest-pinned filename.
recipes/evidence/<recipe>/<src>/is still too vague here. Name the immutable<bundle-digest>.yamlfile so maintainers and the CI gate are talking about the same thing.Suggested fix
- per-source pointer under `recipes/evidence/<recipe>/<src>/`. + per-source pointer file under + `recipes/evidence/<recipe>/<src>/<bundle-digest>.yaml`.🤖 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 `@docs/integrator/recipe-development.md` at line 658, The reference to the per-source pointer under recipes/evidence/<recipe>/<src>/ is too vague; update the documentation in this section to explicitly name the immutable digest-pinned <bundle-digest>.yaml file so readers and CI are referring to the same artifact. Use the existing recipe/evidence path wording around the pointer location, but replace the generic folder-only reference with the exact filename convention.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/scripts/recipe-evidence-check.sh:
- Around line 545-559: The digest status logic in recipe-evidence-check.sh is
using signed_digest even when verification has already failed, which can produce
misleading matches/stale output and extra warnings. Update the digest rendering
block that builds digest_cell so it only compares signed_digest against
current_digest when verify_ok is true, and otherwise emit a
verification-failed/ignored digest state without doing the comparison. Keep the
existing warning counting behavior aligned with the verify_ok branch so a failed
verify does not add a second digest-related warning.
- Around line 71-77: The md_escape helper only strips newline characters, so
verifier-derived values can still include carriage returns and break the
generated Markdown table row. Update md_escape to normalize both newline and
carriage return characters before escaping pipes and removing backticks, keeping
the sanitization logic in the md_escape function consistent for all table-cell
inputs.
- Around line 442-446: The digest computation in the `recipe-evidence-check.sh`
loop still runs `aicr evidence digest` for every protected slug even after the
`MAX_ROWS` cap has effectively been reached. Update the row-processing logic
around `current_digest` and the pointer-row truncation path so the loop stops
invoking the digest command once the cap is hit, while still preserving the
summary/report behavior. Use the existing `MAX_ROWS` control flow and the
per-row handling around the `digest_err`/`current_digest` block to short-circuit
further digest work as soon as no more rows can be emitted.
In `@docs/user/cli-reference.md`:
- Around line 2670-2694: The `aicr evidence sign` docs still point users at the
legacy `recipes/evidence/<recipe>.yaml` target, which is no longer correct under
the per-source pointer contract. Update the synopsis and example text in the
`aicr evidence sign` section to describe the new per-source committed pointer
location and keep the wording aligned with the command’s current contract for
the pointer argument.
In `@pkg/evidence/allowlist/allowlist.go`:
- Around line 121-143: Make allowlist loading cancelable by threading a context
with timeout through the allowlist read path. Update Load in allowlist.go to
accept context.Context and use it to guard the blocking open/read work, then
propagate that context from the only caller in discover.go so the operation can
be bounded and canceled. Keep the existing validation and error wrapping
behavior in Load, but ensure the new context-aware flow is followed from
verifier discovery into allowlist.Load.
In `@pkg/evidence/attestation/pointer_test.go`:
- Around line 87-119: Refactor TestPointerCopyToHint into a table-driven test to
cover both independent cases in one pattern. Keep the existing assertions for
PointerCopyToHint, but express the signed path and unsigned guidance scenarios
as separate table entries with names and expected outcomes. Use the existing
Pointer, PointerAttestation, PointerBundle, and PointerSigner setup within each
case so the test follows the repo’s standard _test.go table-driven style.
In `@pkg/evidence/attestation/pointer.go`:
- Around line 112-115: Update the WritePointerFile docstring to describe the new
per-source layout instead of the old committed pointer path. In the comment near
WritePointerFile, replace the `recipes/evidence/<recipe>.yaml` example with
`recipes/evidence/<recipe>/<source>/<digest>.yaml`, and keep the wording aligned
with the actual in-place patching behavior for committed pointers.
- Around line 122-154: The WritePointerFile helper currently ignores caller
cancellation because it has no context parameter and does not check deadlines
during the temp-file create/write/rename flow. Update WritePointerFile in
pointer.go to accept context.Context and thread it through the pointer
persistence path, using the existing MarshalPointer, os.CreateTemp, tmp.Write,
tmp.Close, and os.Rename steps as checkpoints to abort early when ctx is done.
Make sure every filesystem step returns promptly on cancellation and preserves
the current error-wrapping behavior.
In `@pkg/evidence/attestation/publish.go`:
- Around line 49-53: The NoSign publish flow is logging the wrong copy-to hint
because PointerCopyToHint(pointer) treats an empty signer block as “sign and
push,” even though the bundle has already been pushed. Update the publish path
in publish.go (the NoSign branch and any shared hint helper used there) so the
next-step message distinguishes “unsigned but already pushed” from “not yet
pushed,” and avoid telling users to repeat the push; either special-case
opts.NoSign at the call site or extend PointerCopyToHint to accept that state.
In `@pkg/evidence/verifier/discover.go`:
- Around line 155-183: The pointer validation in checkPointerFile is missing the
filename contract check, so a misnamed YAML file can still be accepted under the
correct recipe/source path. Update checkPointerFile to compare
filepath.Base(path) against the bundle digest declared in the loaded pointer
(the committed pointer name should be <bundle-digest>.yaml), and return an error
when they differ; use the existing LoadAndValidatePointer and ptr fields in
discover.go to enforce this alongside the recipe and source checks.
- Around line 141-147: The directory scan under the evidence tree is too
permissive because the loop in discover.go skips subdirectories and non-“.yaml”
entries, allowing unexpected committed children to evade validation. Update the
file iteration logic that builds TreeProblem entries so every unexpected item
under sourcePath is reported as a problem instead of continuing, and only the
allowed add-only <recipe>/<source>/<bundle-digest>.yaml layout passes. Keep the
existing checkPointerFile path for valid YAML files and add explicit rejection
handling for f.IsDir() and non-YAML names in the same discovery flow.
---
Duplicate comments:
In `@docs/contributor/maintaining.md`:
- Around line 90-92: The pointer-file checklist is too vague and should
explicitly name the immutable digest-pinned artifact. Update the “Pointer file
present” item in the maintaining guide to reference the exact
`<bundle-digest>.yaml` filename under `recipes/evidence/<recipe>/<src>/`,
matching the CI gate and ADR contract. Keep the wording aligned with the
existing checklist entry so it clearly states that each touched overlay must
have the corresponding digest-pinned pointer file.
In `@docs/design/007-recipe-evidence.md`:
- Around line 385-394: The “Protected vs. other affected” bullet still refers to
the generic recipes/evidence/<slug>.yaml pointer, but it should name the
immutable digest-pinned filename used by the CI/verifier contract. Update the
wording in this section to explicitly call out the per-source pinned file path
so the protected-set description matches the rest of the ADR and the verifier
path shape.
In `@docs/integrator/recipe-development.md`:
- Line 658: The reference to the per-source pointer under
recipes/evidence/<recipe>/<src>/ is too vague; update the documentation in this
section to explicitly name the immutable digest-pinned <bundle-digest>.yaml file
so readers and CI are referring to the same artifact. Use the existing
recipe/evidence path wording around the pointer location, but replace the
generic folder-only reference with the exact filename convention.
🪄 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: c08aa453-1c4e-42a8-b733-d98e2855c4db
📒 Files selected for processing (26)
.github/CODEOWNERS.github/scripts/recipe-evidence-check.sh.github/workflows/evidence-pointer-contract.yamldocs/contributor/maintaining.mddocs/design/007-recipe-evidence.mddocs/integrator/recipe-development.mddocs/user/artifact-verification.mddocs/user/cli-reference.mddocs/user/validation.mdpkg/cli/evidence_digest.gopkg/cli/evidence_verify.gopkg/cli/validate.gopkg/evidence/allowlist/allowlist.gopkg/evidence/allowlist/allowlist_test.gopkg/evidence/attestation/emit.gopkg/evidence/attestation/pointer.gopkg/evidence/attestation/pointer_test.gopkg/evidence/attestation/publish.gopkg/evidence/attestation/source.gopkg/evidence/attestation/source_test.gopkg/evidence/verifier/discover.gopkg/evidence/verifier/discover_test.gorecipes/evidence/allowlist.yamlrecipes/evidence/gb200-eks-ubuntu-training/7c4c0edc8c76/sha256-8274b6a1da24aa9782dc12162bf6a38265c30a852585ca64cfad5718efbbdec3.yamlrecipes/evidence/h100-gke-cos-training/7c4c0edc8c76/sha256-33d4cf36622ead990c43a596f6f53c62b87d9fa4708f59b7e3f356f215e54317.yamltools/evidence-pointercheck/main.go
🛑 Comments failed to post (11)
.github/scripts/recipe-evidence-check.sh (3)
71-77: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Collapse carriage returns in Markdown table values.
Line 76 only replaces
\n; verifier-derived hints can still carry\rand disrupt the generated Markdown row.Proposed fix
md_escape() { - printf '%s' "$1" | tr '\n' ' ' | sed 's/|/\\|/g; s/`//g' + printf '%s' "$1" | tr '\n\r' ' ' | sed 's/|/\\|/g; s/`//g' }📝 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.# md_escape sanitizes a value rendered into a Markdown table cell. Slugs and # verifier-supplied hints can in principle carry characters that break the # table or the surrounding backticks; collapse newlines, escape pipes, and # drop backticks so a value can't break out of a code span. md_escape() { printf '%s' "$1" | tr '\n\r' ' ' | sed 's/|/\\|/g; s/`//g' }🤖 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 @.github/scripts/recipe-evidence-check.sh around lines 71 - 77, The md_escape helper only strips newline characters, so verifier-derived values can still include carriage returns and break the generated Markdown table row. Update md_escape to normalize both newline and carriage return characters before escaping pipes and removing backticks, keeping the sanitization logic in the md_escape function consistent for all table-cell inputs.
442-446: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
Skip digest computation after the row cap is reached.
After
MAX_ROWS, this loop still runsaicr evidence digestfor each remaining protected slug before truncating pointer rows. On broad-impact PRs this can waste CI time despite the report cap.Proposed fix
# Compute the recipe's current canonical digest once; every per-source row # compares its signed digest against this. + if [[ "$rows_written" -ge "$MAX_ROWS" ]]; then + rows_truncated=$((rows_truncated + ${`#pointers`[@]})) + continue + fi digest_err=$(mktemp)📝 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.# Compute the recipe's current canonical digest once; every per-source row # compares its signed digest against this. if [[ "$rows_written" -ge "$MAX_ROWS" ]]; then rows_truncated=$((rows_truncated + ${`#pointers`[@]})) continue fi digest_err=$(mktemp) current_digest="" if ! current_digest=$(timeout "$AICR_TIMEOUT" "$AICR" evidence digest -r "$overlay" 2>"$digest_err"); then🤖 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 @.github/scripts/recipe-evidence-check.sh around lines 442 - 446, The digest computation in the `recipe-evidence-check.sh` loop still runs `aicr evidence digest` for every protected slug even after the `MAX_ROWS` cap has effectively been reached. Update the row-processing logic around `current_digest` and the pointer-row truncation path so the loop stops invoking the digest command once the cap is hit, while still preserving the summary/report behavior. Use the existing `MAX_ROWS` control flow and the per-row handling around the `digest_err`/`current_digest` block to short-circuit further digest work as soon as no more rows can be emitted.
545-559: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Don’t render digest trust when verification failed.
If
verify_ok=falsebutsigned_digestis present, this still reportsmatchesorstalefrom an invalid verification result. Skip the digest comparison after verify failure to avoid misleading rows and duplicate warnings.Proposed fix
- if [[ -n "$signed_digest" ]]; then + if [[ "$verify_ok" != "true" ]]; then + digest_cell="—" + elif [[ -n "$signed_digest" ]]; then if [[ "$signed_digest" == "$current_digest" ]]; then digest_cell=":white_check_mark: matches" else digest_cell=":warning: stale (\`${signed_digest:0:12}…\` vs current \`${current_digest:0:12}…\`)" warnings=$((warnings + 1)) fi📝 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.if [[ "$verify_ok" != "true" ]]; then digest_cell="—" elif [[ -n "$signed_digest" ]]; then if [[ "$signed_digest" == "$current_digest" ]]; then digest_cell=":white_check_mark: matches" else digest_cell=":warning: stale (\`${signed_digest:0:12}…\` vs current \`${current_digest:0:12}…\`)" warnings=$((warnings + 1)) fi else digest_cell=":warning: skipped (no signed digest)" # Only a warning when the bundle otherwise verified — a failed verify # already counted its single warning above. if [[ "$verify_ok" == "true" ]]; then warnings=$((warnings + 1)) fi fi🤖 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 @.github/scripts/recipe-evidence-check.sh around lines 545 - 559, The digest status logic in recipe-evidence-check.sh is using signed_digest even when verification has already failed, which can produce misleading matches/stale output and extra warnings. Update the digest rendering block that builds digest_cell so it only compares signed_digest against current_digest when verify_ok is true, and otherwise emit a verification-failed/ignored digest state without doing the comparison. Keep the existing warning counting behavior aligned with the verify_ok branch so a failed verify does not add a second digest-related warning.docs/user/cli-reference.md (1)
2670-2694: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Update the
aicr evidence signpointer path.The synopsis/example still reference the legacy
recipes/evidence/<recipe>.yamllayout. That will send users to the wrong commit target under the new per-source contract.Fix
-The positional `<pointer>` is the committed `recipes/evidence/<recipe>.yaml`. +The positional `<pointer>` is the committed `recipes/evidence/<recipe>/<source>/<digest>.yaml`. ... -aicr evidence sign recipes/evidence/h100-eks-ubuntu-training.yaml +aicr evidence sign recipes/evidence/h100-eks-ubuntu-training/<src>/<digest>.yamlBased on the per-source pointer contract in the PR objectives and line-range notes.
📝 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.The positional `<pointer>` is the committed `recipes/evidence/<recipe>/<source>/<digest>.yaml`. The pointer must carry exactly one attestation that is already pushed (`bundle.oci`/`bundle.digest` set) and not yet signed (empty `signer`); otherwise the command fails closed (an already-signed pointer is never re-signed). **Flags:** | Flag | Alias | Type | Default | Description | |------|-------|------|---------|-------------| | `--identity-token` | | string | | Pre-fetched OIDC identity token for keyless signing. Skips ambient/browser/device-code flows. Reads `COSIGN_IDENTITY_TOKEN` from env. Same precedence chain as `aicr evidence publish`. | | `--oidc-device-flow` | | bool | `false` | Use the OAuth 2.0 device authorization grant for OIDC instead of opening a browser callback. Reads `AICR_OIDC_DEVICE_FLOW`. Useful on headless hosts. | | `--yes` | `--assume-yes` | bool | `false` | Skip the interactive confirmation shown before keyless signing publishes your OIDC identity (browser/device-code paths only; the banner is still printed). Reads `AICR_ASSUME_YES`. | | `--plain-http` | | bool | `false` | Use HTTP instead of HTTPS for the registry (pull + referrer attach; local-registry tests). | | `--insecure-tls` | | bool | `false` | Skip TLS verification for the registry (pull + referrer attach; self-signed registries). | **Exit codes:** | Code | Meaning | |------|---------| | 0 | Bundle signed, referrer attached, and the pointer's `signer` block written back. | | non-zero | Pointer already signed / has nothing pushed to sign, bundle could not be pulled (e.g. a private registry returns 403), identity-disclosure prompt declined, or signing/attach failed. | **Examples:**🤖 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 `@docs/user/cli-reference.md` around lines 2670 - 2694, The `aicr evidence sign` docs still point users at the legacy `recipes/evidence/<recipe>.yaml` target, which is no longer correct under the per-source pointer contract. Update the synopsis and example text in the `aicr evidence sign` section to describe the new per-source committed pointer location and keep the wording aligned with the command’s current contract for the pointer argument.pkg/evidence/allowlist/allowlist.go (1)
121-143: 🩺 Stability & Availability | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
git ls-files pkg/evidence/allowlist/allowlist.go && wc -l pkg/evidence/allowlist/allowlist.go && sed -n '1,220p' pkg/evidence/allowlist/allowlist.goRepository: NVIDIA/aicr
Length of output: 8164
🏁 Script executed:
rg -n "\bLoad\(" pkg/evidence -g '!**/*_test.go' && printf '\n---\n' && rg -n "context\.Context|WithTimeout|ReadAll|Open\(" pkg/evidence/allowlist/allowlist.go pkg/evidence -g '!**/*_test.go'Repository: NVIDIA/aicr
Length of output: 6570
🏁 Script executed:
sed -n '220,420p' pkg/evidence/allowlist/allowlist.goRepository: NVIDIA/aicr
Length of output: 6544
🏁 Script executed:
rg -n "allowlist\.Load|LoadAllowlist|allowlist" pkg -g '!**/*_test.go'Repository: NVIDIA/aicr
Length of output: 10194
🏁 Script executed:
python3 - <<'PY' from pathlib import Path import re root = Path('.') paths = [p for p in root.rglob('*.go') if 'vendor' not in p.parts] targets = [] for p in paths: try: text = p.read_text() except Exception: continue if re.search(r'\ballowlist\.Load\s*\(', text) or re.search(r'\bLoad\s*\(\s*path string\s*\)\s*\(\*Allowlist,\s*error\)', text): targets.append(str(p)) print("\n".join(sorted(set(targets)))) PYRepository: NVIDIA/aicr
Length of output: 219
Make allowlist loading cancelable.
pkg/evidence/allowlist/allowlist.go:121does blockingos.Open/io.ReadAllwith nocontext.Context, and the only caller (pkg/evidence/verifier/discover.go:81) also lacks one. Thread a timeout-bounded context through this path.🤖 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 `@pkg/evidence/allowlist/allowlist.go` around lines 121 - 143, Make allowlist loading cancelable by threading a context with timeout through the allowlist read path. Update Load in allowlist.go to accept context.Context and use it to guard the blocking open/read work, then propagate that context from the only caller in discover.go so the operation can be bounded and canceled. Keep the existing validation and error wrapping behavior in Load, but ensure the new context-aware flow is followed from verifier discovery into allowlist.Load.Source: Coding guidelines
pkg/evidence/attestation/pointer_test.go (1)
87-119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Use a table-driven test here.
This function is validating two independent cases (signed path and unsigned guidance), so it should use the repo's table-driven test pattern. As per coding guidelines,
**/*_test.go:Write table-driven tests for multiple test cases.🤖 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 `@pkg/evidence/attestation/pointer_test.go` around lines 87 - 119, Refactor TestPointerCopyToHint into a table-driven test to cover both independent cases in one pattern. Keep the existing assertions for PointerCopyToHint, but express the signed path and unsigned guidance scenarios as separate table entries with names and expected outcomes. Use the existing Pointer, PointerAttestation, PointerBundle, and PointerSigner setup within each case so the test follows the repo’s standard _test.go table-driven style.Source: Coding guidelines
pkg/evidence/attestation/pointer.go (2)
112-115: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Update this docstring to the new per-source layout.
The comment still points to
recipes/evidence/<recipe>.yaml, but this PR's contract isrecipes/evidence/<recipe>/<source>/<digest>.yaml. Leaving the old path here will send future callers to the wrong on-disk location.🤖 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 `@pkg/evidence/attestation/pointer.go` around lines 112 - 115, Update the WritePointerFile docstring to describe the new per-source layout instead of the old committed pointer path. In the comment near WritePointerFile, replace the `recipes/evidence/<recipe>.yaml` example with `recipes/evidence/<recipe>/<source>/<digest>.yaml`, and keep the wording aligned with the actual in-place patching behavior for committed pointers.
122-154: 🩺 Stability & Availability | 🟡 Minor
🧩 Analysis chain
🏁 Script executed:
# Inspect the attestation pointer implementation and its callers. set -e git ls-files 'pkg/evidence/attestation/*' 'pkg/evidence/**' | sed -n '1,200p' ast-grep outline pkg/evidence/attestation/pointer.go --view expanded || true wc -l pkg/evidence/attestation/pointer.go cat -n pkg/evidence/attestation/pointer.go | sed -n '1,240p' rg -n "WritePointerFile\(" -S .Repository: NVIDIA/aicr
Length of output: 10608
🏁 Script executed:
sed -n '1,260p' pkg/evidence/attestation/sign.go printf '\n---\n' sed -n '1,260p' pkg/evidence/attestation/publish.go printf '\n---\n' sed -n '1,260p' pkg/evidence/attestation/emit.goRepository: NVIDIA/aicr
Length of output: 27043
🏁 Script executed:
sed -n '1,260p' pkg/evidence/attestation/sign.go printf '\n---\n' sed -n '1,260p' pkg/evidence/attestation/publish.go printf '\n---\n' sed -n '1,260p' pkg/evidence/attestation/emit.goRepository: NVIDIA/aicr
Length of output: 27043
🏁 Script executed:
rg -n "context\.WithTimeout|CreateTemp\(|os\.Rename\(|io\.ReadAll\(|WritePointerFile\(" pkg/evidence pkg -g '!**/*_test.go'Repository: NVIDIA/aicr
Length of output: 15831
🏁 Script executed:
rg -n "context\.WithTimeout|CreateTemp\(|os\.Rename\(|io\.ReadAll\(|WritePointerFile\(" pkg/evidence pkg -g '!**/*_test.go'Repository: NVIDIA/aicr
Length of output: 15831
🏁 Script executed:
rg -n "context\\.WithTimeout|CreateTemp\\(|os\\.Rename\\(|io\\.ReadAll\\(|WritePointerFile\\(" pkg/evidence pkg -g '!**/*_test.go'Repository: NVIDIA/aicr
Length of output: 15831
🏁 Script executed:
rg -n "context\\.WithTimeout|CreateTemp\\(|os\\.Rename\\(|io\\.ReadAll\\(|WritePointerFile\\(" pkg/evidence pkg -g '!**/*_test.go'Repository: NVIDIA/aicr
Length of output: 15831
Thread
context.ContextthroughWritePointerFile. The temp-file create/write/rename path drops the caller’s deadline, so pointer persistence can’t be canceled alongside the rest of the evidence workflow. Passctxinto this helper and check it before each filesystem step.🤖 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 `@pkg/evidence/attestation/pointer.go` around lines 122 - 154, The WritePointerFile helper currently ignores caller cancellation because it has no context parameter and does not check deadlines during the temp-file create/write/rename flow. Update WritePointerFile in pointer.go to accept context.Context and thread it through the pointer persistence path, using the existing MarshalPointer, os.CreateTemp, tmp.Write, tmp.Close, and os.Rename steps as checkpoints to abort early when ctx is done. Make sure every filesystem step returns promptly on cancellation and preserves the current error-wrapping behavior.Source: Coding guidelines
pkg/evidence/attestation/publish.go (1)
49-53: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Fix the
copyTohint forNoSignpublishes.With
NoSign, this path now logsPointerCopyToHint(pointer), but that helper falls back to “sign and push” whenever the signer block is empty. In the new workflow the bundle is already pushed, so the logged next step is wrong and tells users to repeat an unnecessary push. Either special-caseopts.NoSignhere or teach the helper to distinguish “unsigned but already pushed” from “not yet pushed”.Suggested fix
- slog.Info("evidence pointer written", - "path", pointerPath, - "copyTo", PointerCopyToHint(pointer)) + copyTo := PointerCopyToHint(pointer) + if opts.NoSign && out.PushSummary != nil { + copyTo = "(sign the pushed bundle, then commit under recipes/evidence/<recipe>/<source>/)" + } + slog.Info("evidence pointer written", + "path", pointerPath, + "copyTo", copyTo)Also applies to: 125-127
🤖 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 `@pkg/evidence/attestation/publish.go` around lines 49 - 53, The NoSign publish flow is logging the wrong copy-to hint because PointerCopyToHint(pointer) treats an empty signer block as “sign and push,” even though the bundle has already been pushed. Update the publish path in publish.go (the NoSign branch and any shared hint helper used there) so the next-step message distinguishes “unsigned but already pushed” from “not yet pushed,” and avoid telling users to repeat the push; either special-case opts.NoSign at the call site or extend PointerCopyToHint to accept that state.pkg/evidence/verifier/discover.go (2)
141-147: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Reject unexpected children under
<recipe>/<source>/.Line 142 silently skips subdirectories and non-
.yamlfiles. That lets a committed file likerecipes/evidence/<recipe>/<source>/nested/evil.yamlevade this gate entirely even though the contract is supposed to cover every committed pointer under the evidence tree. Treat unexpected entries here as problems instead ofcontinue.Based on the PR objectives, the allowed layout is the add-only
<recipe>/<source>/<bundle-digest>.yamlcontract.Proposed fix
for _, f := range files { - if f.IsDir() || !strings.HasSuffix(f.Name(), ".yaml") { + if f.IsDir() { + problems = append(problems, TreeProblem{ + Path: filepath.Join(sourcePath, f.Name()), + Message: "unexpected nested directory under <recipe>/<source>/", + }) + continue + } + if !strings.HasSuffix(f.Name(), ".yaml") { + problems = append(problems, TreeProblem{ + Path: filepath.Join(sourcePath, f.Name()), + Message: "unexpected file under <recipe>/<source>/; pointers must be named <bundle-digest>.yaml", + }) continue } path := filepath.Join(sourcePath, f.Name())📝 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.for _, f := range files { if f.IsDir() { problems = append(problems, TreeProblem{ Path: filepath.Join(sourcePath, f.Name()), Message: "unexpected nested directory under <recipe>/<source>/", }) continue } if !strings.HasSuffix(f.Name(), ".yaml") { problems = append(problems, TreeProblem{ Path: filepath.Join(sourcePath, f.Name()), Message: "unexpected file under <recipe>/<source>/; pointers must be named <bundle-digest>.yaml", }) continue } path := filepath.Join(sourcePath, f.Name()) if msg := checkPointerFile(path, recipe, source, al); msg != "" { problems = append(problems, TreeProblem{Path: path, Message: msg})🤖 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 `@pkg/evidence/verifier/discover.go` around lines 141 - 147, The directory scan under the evidence tree is too permissive because the loop in discover.go skips subdirectories and non-“.yaml” entries, allowing unexpected committed children to evade validation. Update the file iteration logic that builds TreeProblem entries so every unexpected item under sourcePath is reported as a problem instead of continuing, and only the allowed add-only <recipe>/<source>/<bundle-digest>.yaml layout passes. Keep the existing checkPointerFile path for valid YAML files and add explicit rejection handling for f.IsDir() and non-YAML names in the same discovery flow.
155-183: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Enforce the
<bundle-digest>.yamlfilename contract.
checkPointerFileverifies recipe and source, but it never comparesfilepath.Base(path)with the digest declared in the pointer.anything.yamlunder the right<recipe>/<source>/therefore passes, which weakens the immutable per-run path contract and allows misnamed or duplicate pointers for the same bundle.Based on the PR objectives, each committed pointer path is part of the contract and should be
<bundle-digest>.yaml.Proposed fix
if wantSource != source { return "signer " + signer.Identity + " maps to source " + wantSource + " but the pointer is committed under " + source + " (path ownership / squat)" } + wantName := strings.ReplaceAll(ptr.Attestations[0].Bundle.Digest, ":", "-") + ".yaml" + if filepath.Base(path) != wantName { + return "pointer filename " + filepath.Base(path) + " does not match bundle digest " + wantName + } class, _, ok := al.Classify(signer.Issuer, signer.Identity)📝 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.func checkPointerFile(path, recipe, source string, al *allowlist.Allowlist) string { ptr, err := LoadAndValidatePointer(path) if err != nil { return "invalid pointer: " + err.Error() } if ptr.Recipe != recipe { return "pointer.recipe " + ptr.Recipe + " does not match recipe directory " + recipe } signer := ptr.Attestations[0].Signer if signer == nil { return "committed pointer must be signed (attestation has no signer)" } wantSource, err := attestation.SourceSlug(signer.Issuer, signer.Identity) if err != nil { return "cannot derive source slug from signer: " + err.Error() } if wantSource != source { return "signer " + signer.Identity + " maps to source " + wantSource + " but the pointer is committed under " + source + " (path ownership / squat)" } wantName := strings.ReplaceAll(ptr.Attestations[0].Bundle.Digest, ":", "-") + ".yaml" if filepath.Base(path) != wantName { return "pointer filename " + filepath.Base(path) + " does not match bundle digest " + wantName } class, _, ok := al.Classify(signer.Issuer, signer.Identity) if !ok { return "signer " + signer.Identity + " (issuer " + signer.Issuer + ") is not in the allowlist; add a community/partner entry to recipes/evidence/allowlist.yaml" } if class == allowlist.ClassFirstParty { return "first-party signer must ingest directly, not commit a per-run pointer" } return "" }🤖 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 `@pkg/evidence/verifier/discover.go` around lines 155 - 183, The pointer validation in checkPointerFile is missing the filename contract check, so a misnamed YAML file can still be accepted under the correct recipe/source path. Update checkPointerFile to compare filepath.Base(path) against the bundle digest declared in the loaded pointer (the committed pointer name should be <bundle-digest>.yaml), and return an error when they differ; use the existing LoadAndValidatePointer and ptr fields in discover.go to enforce this alongside the recipe and source checks.
… per-class allowlist) Remediate CodeRabbit review on the per-source pointer contract: - Widen SourceSlugLength 12->32 (48->128 bit). The slug is an authorization key (allowlist.Classify + verifier path-ownership), so it must resist a deliberate second-preimage squat. Rename committed evidence dirs and update fixtures/docs/allowlist accordingly; leave the unrelated OCI manifest fingerprint at 12 hex. - Anchor allowlist Classify slug match on issuer as well, so a colliding slug under a different issuer cannot inherit an entry. - Enforce per-class key kind in validateEntry: first-party uses identityPattern, community/partner use source; add negative tests. - Preserve allowlist.Load's coded errors in CheckEvidenceTree via PropagateOrWrap. - Add a Pointer column to the recipe-evidence report so multiple pointers under one source stay distinguishable. - Docs: name the <bundle-digest>.yaml pointer file in maintaining.md and add a language tag to the ADR-007 path fence.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
docs/contributor/maintaining.md (1)
242-245: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAdd a language tag to this fence.
markdownlint-cli2will flag the bare fenced block here;textis enough for a path listing.🤖 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 `@docs/contributor/maintaining.md` around lines 242 - 245, The fenced block in the contributor maintenance docs is missing a language tag and will be flagged by markdownlint-cli2; update that specific fence in the maintaining guide to use a text language label since it contains a path listing. Locate the bare markdown fence in the docs/maintaining section and add the appropriate language identifier without changing the content.docs/design/007-recipe-evidence.md (1)
242-245: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winTag both path-listing fences as
text.These bare fences will trip markdownlint;
textkeeps the snippet readable and lint-clean.Also applies to: 717-720
🤖 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 `@docs/design/007-recipe-evidence.md` around lines 242 - 245, The markdown examples use bare fenced code blocks, which will trip markdownlint; update the fenced snippets in the referenced example sections to explicitly declare text as the language while keeping the existing content unchanged. Use the nearby snippet markers around the registry and certificateIdentityRegexp examples, and apply the same fix to the other mentioned path-listing fence as well.
🤖 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 @.github/scripts/recipe-evidence-check.sh:
- Around line 410-411: The report table header is currently only printed to
stdout, while the row entries are appended to the report file, so the report can
miss its Markdown header. Update the header emission in the table-building
section of recipe-evidence-check.sh so the same output path used by the later
row writes is also used for the header lines, keeping the report structure
consistent.
---
Duplicate comments:
In `@docs/contributor/maintaining.md`:
- Around line 242-245: The fenced block in the contributor maintenance docs is
missing a language tag and will be flagged by markdownlint-cli2; update that
specific fence in the maintaining guide to use a text language label since it
contains a path listing. Locate the bare markdown fence in the docs/maintaining
section and add the appropriate language identifier without changing the
content.
In `@docs/design/007-recipe-evidence.md`:
- Around line 242-245: The markdown examples use bare fenced code blocks, which
will trip markdownlint; update the fenced snippets in the referenced example
sections to explicitly declare text as the language while keeping the existing
content unchanged. Use the nearby snippet markers around the registry and
certificateIdentityRegexp examples, and apply the same fix to the other
mentioned path-listing fence as well.
🪄 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: 383d1c91-fef4-4295-ac45-6c9434acb2ef
📒 Files selected for processing (15)
.github/scripts/recipe-evidence-check.shdocs/contributor/maintaining.mddocs/design/007-recipe-evidence.mddocs/user/artifact-verification.mddocs/user/cli-reference.mdpkg/evidence/allowlist/allowlist.gopkg/evidence/allowlist/allowlist_test.gopkg/evidence/attestation/pointer_test.gopkg/evidence/attestation/source.gopkg/evidence/attestation/source_test.gopkg/evidence/verifier/discover.gopkg/evidence/verifier/discover_test.gorecipes/evidence/allowlist.yamlrecipes/evidence/gb200-eks-ubuntu-training/7c4c0edc8c765a95a0f3afdb3bbb8e91/sha256-8274b6a1da24aa9782dc12162bf6a38265c30a852585ca64cfad5718efbbdec3.yamlrecipes/evidence/h100-gke-cos-training/7c4c0edc8c765a95a0f3afdb3bbb8e91/sha256-33d4cf36622ead990c43a596f6f53c62b87d9fa4708f59b7e3f356f215e54317.yaml
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/user/cli-reference.md`:
- Line 389: Update the `--service` option description in the CLI reference to
include `any` in the supported values list, since `service=any` is already used
for self-managed clusters in the validation flow. Locate the existing
`--service` table entry in the CLI reference and expand the allowed service
values alongside the current list so the documented validation matches the
`service=any` example used elsewhere.
🪄 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: 9f629a91-636a-4056-9feb-f868e176cafa
📒 Files selected for processing (3)
docs/contributor/maintaining.mddocs/user/cli-reference.mddocs/user/validation.md
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/user/cli-reference.md`:
- Line 389: Update the `--service` option description in the CLI reference to
include `any` in the supported values list, since `service=any` is already used
for self-managed clusters in the validation flow. Locate the existing
`--service` table entry in the CLI reference and expand the allowed service
values alongside the current list so the documented validation matches the
`service=any` example used elsewhere.
🪄 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: 9f629a91-636a-4056-9feb-f868e176cafa
📒 Files selected for processing (3)
docs/contributor/maintaining.mddocs/user/cli-reference.mddocs/user/validation.md
🛑 Comments failed to post (1)
docs/user/cli-reference.md (1)
389-389: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Add
anyto the supported--servicelist.The validation walkthrough already uses
service=anyfor self-managed clusters, so omitting it here makes a valid input look unsupported. Based on theservice=anyexample indocs/user/validation.md.Suggested fix
-| `--service` | | string | K8s service: eks, gke, aks, oke, ocp, kind, lke, bcm | +| `--service` | | string | K8s service: eks, gke, aks, oke, ocp, any, kind, lke, bcm |📝 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.| `--service` | | string | K8s service: eks, gke, aks, oke, ocp, any, kind, lke, bcm |🤖 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 `@docs/user/cli-reference.md` at line 389, Update the `--service` option description in the CLI reference to include `any` in the supported values list, since `service=any` is already used for self-managed clusters in the validation flow. Locate the existing `--service` table entry in the CLI reference and expand the allowed service values alongside the current list so the documented validation matches the `service=any` example used elsewhere.
mchmarny
left a comment
There was a problem hiding this comment.
Trust-root code that fails closed correctly. The source slug is non-squattable (128-bit SHA over issuer\nidentity with a domain separator), the allowlist loader strict-decodes and rejects any cleartext identity:, reads are bounded with io.LimitReader, and the path-ownership gate enforces content-addressed ownership (recomputed slug equality) rather than a .. substring scan — so a pointer cannot be placed under another party's directory and traversal is structurally impossible. Test coverage exercises the security-critical shapes: squat, unsigned, unlisted, recipe-mismatch, loose-file, over-broad pattern, two-sources-one-recipe, and domain-separation.
No actionable findings — the remaining observations are informational (slug width is sound against second-preimage; the ctx-less CLI reads fall within the project's CLI/test carve-out and match the existing LoadAndValidatePointer signature).
One CI note: the lone red check (KWOK Test Summary → eks-inference (flux-git)) is an unrelated flake — this PR touches no recipe/registry/overlay/component file, and all five sibling eks-inference deployer legs (argocd-git, argocd-helm-oci, argocd-oci, flux-oci, helm) passed. Worth a re-run; not a code signal.
LGTM.
Summary
Establishes the in-repo
recipes/evidence/per-source pointer contract (#1347, Option A): one immutable single-attestation pointer per run atrecipes/evidence/<recipe>/<src>/<bundle-digest>.yaml, plus a maintained signer allowlist and CI path-ownership enforcement.Motivation / Context
Evidence pointers were keyed solely by recipe slug, so a second contributor's submission for the same recipe silently overwrote the first. This adopts the #1347 directory-per-source layout (add-only, immutable), where
<src>is a one-way slug derived from the verified signer OIDC identity — so two sources for one recipe never collide and the path is not squattable. It also stands up the maintained signer allowlist (the trust root for the corroboration dashboard, epic #1400) and the path-ownership CI gate.Fixes: #1401
Related: #1347, #1400
Type of Change
Component(s) Affected
cmd/aicr,pkg/cli)docs/,examples/)pkg/evidence/*,recipes/evidence/,.github/(CI + CODEOWNERS)Implementation Notes
attestation.SourceSlug(issuer, identity)= first 12 hex ofsha256(issuer + "\n" + identity). Deriving<src>from the signer (not a free-form label) is what makes the path non-squattable and keeps it aligned with the GP2 ingest keying.git mv, content unchanged — already single-attestation).pkg/evidence/allowlist+recipes/evidence/allowlist.yaml— the trust root. Classesfirst-party/community/partner, validated disjoint and non-overlapping. Community/partner entries are slug-only (source+issuer+ optional non-PIIlabel) so no cleartext contributor identity is committed; the loader rejects a strayidentity:field. First-party entries pin tightly-bounded workflowidentityPatterns (CI URLs, not personal PII). A verified-but-unlisted signer is admitted as reported, never corroborating.verifier.DiscoverPointers(glob<recipe>/*/*.yaml) for multi-source discovery, andverifier.CheckEvidenceTreefor the anti-squat gate: a pointer must be signed, hash to the<src>directory it lives in, match its recipe directory, and be allowlisted community/partner.tools/evidence-pointercheck+ theevidence-pointer-contractworkflow run the gate offline (no OCI fetch — fork-safe).CODEOWNERSmakes maintainer review the trust gate. The warning-onlyrecipe-evidence-check.shand the emit/publishcopyTohint were generalized to the per-source layout.main); committed pointers are the community/partner channel.Testing
All green. New-code coverage:
allowlist92.9%,SourceSlug100%,discover.go84–100%. (Race detector requires cgo/gcc, unavailable in the dev sandbox; CI runs-race.)Tests cover the required cases: pointer parse/validate, two-sources-one-recipe glob discovery (no overwrite), path-ownership/squat rejection, allowlist schema (slug format, disjoint, no over-broad pattern, rejects cleartext identity), and deterministic pointer YAML.
Risk Assessment
Rollout notes: First-party direct-ingest is wired by GP2 (separate); until then UAT continues to upload its pointer as a CI artifact (it does not commit per-run). No migration required for existing consumers.
Checklist
make testwith-race) — non-race locally (no cgo in sandbox); CI runs-racemake lint)git commit -S)