Problem
Recipe resolution does not validate that a resolved ComponentRef has a coherent deployment type/field combination. mergeComponentRef merges each field (Type, Version, Tag, Path, Source, Chart) independently, and ApplyRegistryDefaults only fills empty fields — so a recipe can resolve to an incoherent ref, e.g.:
Type: Helm that also carries a Kustomize Tag or Path
- a Kustomize ref (or one with
Tag/Path) that lacks the Path Kustomize requires
- a
Tag with no Source/Repository
- a Kustomize ref that also declares post-manifests (
ManifestFiles)
- an unsupported/empty
Type on an externally-supplied ref
Nothing rejects these at resolution — or at the load/adopt boundaries.
Impact
- Deployers build a different type than the recipe declares (primary). The deployers do not trust the declared
Type: Helm/Helmfile/ArgoCD drop it, and localformat.classify (pkg/bundler/deployer/localformat/writer.go) treats any ref carrying a Tag/Path as Kustomize. So an incoherent ref silently deploys as a different type than authored — and this runs on every aicr bundle (not just validate/attestation).
- Signed attestation records the wrong metadata (second-order).
BuildAutoBOM (pkg/evidence/attestation/bom.go) selects the pinned version/source by the ref's declared Type, so the CycloneDX attestation — a signed supply-chain artifact — advertises metadata that does not match what actually deploys.
- Reachable via external recipes. The main exposure is externally-authored hydrated
RecipeResults (aicr bundle/validate -r recipe.yaml, POST /v1/bundle), which never pass through the resolver. No in-tree recipe is incoherent today (zero Kustomize components in the registry).
Surfaced during the cross-review of #1580.
Fix (implemented in #1585)
A shared RecipeResult.PrepareAndValidate() (back-fill missing types → canonicalize → ValidateCoherence) invoked at every boundary that produces or consumes a RecipeResult, so no path is a bypass:
finalizeRecipeResult (criteria resolution), after applyRegistryDefaults populates Type;
LoadFromFileWithProvider (a hydrated recipe.yaml read from disk); and
- the client
adoptRecipe path (POST /v1/bundle decodes a RecipeResult).
- the public
DefaultBundler.Make (pkg/bundler) entry point — reachable without the CLI/server boundaries (its Quick Start calls Make directly); validates a provider-preserving defensive copy before generating.
Rules mirror the deployer requirements in pkg/bundler/deployer/localformat:
- a Helm ref must not carry
Tag/Path;
- a Kustomize ref needs a
Path;
- a Kustomize
Tag needs a Source;
- a Kustomize ref must not also declare post-manifests (
ManifestFiles) — PreManifestFiles remain supported;
- an unsupported/empty
Type fails closed.
Only enabled refs are checked (disabled stubs are excluded from the bundle); offenders are aggregated into one ErrCodeInvalidRequest.
Additional refinements from cross-review of #1585:
- Type is matched case-insensitively. The resolver emits canonical
Helm/Kustomize, but the REST wire format and hand-authored recipes may use lowercase; since the deployers classify by tag/path rather than this field, helm and Helm are treated the same, so the check does not newly reject the documented lowercase wire form.
- OpenAPI contract updated (
api/aicr/v1/server.yaml): the componentRefs schema previously omitted type/source/chart/tag/path; those are now documented (with the type enum and coherence-rule note) and the /v1/bundle example canonicalized to type: Helm, so spec-following clients can populate the fields the check validates. Includes a REST/adopt-boundary test.
Acceptance
- Resolving, loading, or adopting a recipe with an incoherent (enabled)
ComponentRef fails with a clear ErrCodeInvalidRequest naming the offending field combination. ✅
- The resolver's rules match
localformat's. ✅ (kept in lockstep by comment; a fully-shared predicate across the two different Component/ComponentRef types is a possible future refactor)
- Every existing overlay/mixin still resolves — verified by the
pkg/recipe suite and the bundler/client resolution consumers. ✅
- Unit tests cover each rejected combination and the coherent cases. ✅
Problem
Recipe resolution does not validate that a resolved
ComponentRefhas a coherent deploymenttype/field combination.mergeComponentRefmerges each field (Type,Version,Tag,Path,Source,Chart) independently, andApplyRegistryDefaultsonly fills empty fields — so a recipe can resolve to an incoherent ref, e.g.:Type: Helmthat also carries a KustomizeTagorPathTag/Path) that lacks thePathKustomize requiresTagwith noSource/RepositoryManifestFiles)Typeon an externally-supplied refNothing rejects these at resolution — or at the load/adopt boundaries.
Impact
Type: Helm/Helmfile/ArgoCD drop it, andlocalformat.classify(pkg/bundler/deployer/localformat/writer.go) treats any ref carrying aTag/Pathas Kustomize. So an incoherent ref silently deploys as a different type than authored — and this runs on everyaicr bundle(not just validate/attestation).BuildAutoBOM(pkg/evidence/attestation/bom.go) selects the pinned version/source by the ref's declaredType, so the CycloneDX attestation — a signed supply-chain artifact — advertises metadata that does not match what actually deploys.RecipeResults (aicr bundle/validate -r recipe.yaml,POST /v1/bundle), which never pass through the resolver. No in-tree recipe is incoherent today (zero Kustomize components in the registry).Surfaced during the cross-review of #1580.
Fix (implemented in #1585)
A shared
RecipeResult.PrepareAndValidate()(back-fill missing types → canonicalize →ValidateCoherence) invoked at every boundary that produces or consumes aRecipeResult, so no path is a bypass:finalizeRecipeResult(criteria resolution), afterapplyRegistryDefaultspopulatesType;LoadFromFileWithProvider(a hydratedrecipe.yamlread from disk); andadoptRecipepath (POST /v1/bundledecodes aRecipeResult).DefaultBundler.Make(pkg/bundler) entry point — reachable without the CLI/server boundaries (its Quick Start callsMakedirectly); validates a provider-preserving defensive copy before generating.Rules mirror the deployer requirements in
pkg/bundler/deployer/localformat:Tag/Path;Path;Tagneeds aSource;ManifestFiles) —PreManifestFilesremain supported;Typefails closed.Only enabled refs are checked (disabled stubs are excluded from the bundle); offenders are aggregated into one
ErrCodeInvalidRequest.Additional refinements from cross-review of #1585:
Helm/Kustomize, but the REST wire format and hand-authored recipes may use lowercase; since the deployers classify bytag/pathrather than this field,helmandHelmare treated the same, so the check does not newly reject the documented lowercase wire form.api/aicr/v1/server.yaml): thecomponentRefsschema previously omittedtype/source/chart/tag/path; those are now documented (with thetypeenum and coherence-rule note) and the/v1/bundleexample canonicalized totype: Helm, so spec-following clients can populate the fields the check validates. Includes a REST/adopt-boundary test.Acceptance
ComponentReffails with a clearErrCodeInvalidRequestnaming the offending field combination. ✅localformat's. ✅ (kept in lockstep by comment; a fully-shared predicate across the two differentComponent/ComponentReftypes is a possible future refactor)pkg/recipesuite and the bundler/client resolution consumers. ✅