Skip to content

feat: explode archive materials into named contract slots with a prefix policy selector - #3314

Merged
javirln merged 15 commits into
chainloop-dev:mainfrom
javirln:javier/explode-named-contract-slots
Jul 30, 2026
Merged

feat: explode archive materials into named contract slots with a prefix policy selector#3314
javirln merged 15 commits into
chainloop-dev:mainfrom
javirln:javier/explode-named-contract-slots

Conversation

@javirln

@javirln javirln commented Jul 29, 2026

Copy link
Copy Markdown
Member

Summary

Makes chainloop attestation add usable with fixed, named contracts when a single logical input is delivered as an archive of many same-kind files (for example a bundle of SARIF reports). Today an archive can be exploded into many materials, but the resulting materials get generic auto-generated names and therefore cannot satisfy a contract's required, named material slot, nor be targeted by a name-based policy selector. This change closes that gap.

Three additive, backward-compatible pieces.

1. Stable, reproducible naming for exploded materials

When --value is an explodable archive (.zip / .tar / .tar.gz) for an explodable kind, the CLI now:

  • sorts the archive entries by their normalized path, deterministically and independent of the order the archive was written in;
  • names the first entry with the exact --name, and the rest <name>-1, <name>-2, …

The first entry therefore fills a required, named contract slot, and the file-to-name mapping is fully reproducible for a given archive (no timestamps or randomness).

chainloop attestation add --kind SARIF --name scan-report --value scans.zip
# -> scan-report, scan-report-1, scan-report-2, ...

2. The source archive is recorded as evidence

The original archive is recorded once as an EVIDENCE material and cross-linked with every exploded material in both directions via the chainloop.material.references annotation, so the source bundle itself is attested and traceable to the materials derived from it.

3. Prefix match mode for material selectors

Adds an additive match_mode to the contract material selector — EXACT (default) and PREFIX — on both standalone policy attachments (PolicyAttachment.MaterialSelector) and policy groups (PolicyGroup.Material). A single shared predicate drives both paths.

PREFIX matches the exact name or a <name>-<suffix> derivative, so a selector can target an entire exploded set:

policies:
  materials:
    - ref: my-policy
      selector:
        name: scan-report
        match_mode: PREFIX   # matches scan-report, scan-report-1, ...

PREFIX is a literal prefix — it matches any material whose name begins with the value — so choose a discriminating prefix (typically the full slot name, e.g. scan-report, which matches scan-report, scan-report-1, …).

How policies apply to the exploded set

A policy attachment applies to a material when the policy's declared kind matches the material's type and the selector, if present, matches the name. Evaluation is per material: N matching materials times M attachments produce N×M independent evaluations, each recorded against its own material. Exploding N reports into a prefix-selected slot therefore runs every attachment against every report. This also supports multi-slot contracts where each role has its own prefix selector (for example a build-time scan slot and a stripped-release scan slot): the two prefixes never cross-match, so each role's policies apply only to that role's materials.

What this covers

  • Explode into a required, named contract slot, and multi-slot contracts via per-role prefix selectors.
  • .zip, .tar, and .tar.gz; deterministic ordering; --name collisions; user --annotations propagated to every exploded material; archive-as-evidence cross-linking in both directions.
  • Backward compatible: existing selectors with no match_mode keep exact-match behavior, and adding a single (non-archive) material is unchanged.

What this does not cover

  • Nested archives are not recursed: a .zip inside the archive is treated as a single regular file.
  • Archive-native kinds (for example ZAP_DAST_ZIP) are still recorded whole, not exploded.
  • If --name collides with a material already present in the attestation, the allocator never overwrites it, so the first exploded entry becomes <name>-1 and will not fill an exact required slot named <name> in that case.
  • No merging of file contents: each exploded entry stays its own material with its own digest.

Compatibility

The proto change is purely additive (a new optional enum field with UNSPECIFIED = 0 decoding to exact match). Contracts are persisted as raw body plus serialized proto, so no data migration is required and existing contracts deserialize unchanged.

AI assistance: Claude Code.

javirln added 10 commits July 29, 2026 15:32
A policy attachment's material selector now supports a match mode. The
default (unspecified) remains an exact name match, so existing contracts
are unchanged; PREFIX matches every material whose name starts with the
selector name, letting a single attachment target a set of related
materials (e.g. an archive exploded into "<name>", "<name>-1", …).

Assisted-by: Claude Code
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>

Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
Archive explosion now names the first entry (in sorted path order) with
the exact --name and the rest with positional suffixes "<name>-1",
"<name>-2", …. Entries are sorted by their normalized path before naming
so the file-to-name mapping is deterministic and reproducible regardless
of how the archive stored them, and the first name lines up with a fixed
contract's named required slot.

Assisted-by: Claude Code
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>

Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
When `att add` explodes an archive, the original archive is now recorded
once as an EVIDENCE material and cross-linked with every exploded
material in both directions via chainloop.material.references, so the
source bundle itself is attested.

Assisted-by: Claude Code
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>

Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
- extract materials.NormalizeArchivePath and reuse it (safeArchivePath,
  ArchiveEntryBaseName, and the explode sort key), computed once per entry
- drop the now-dead AllocateSequential and its seq field (explode uses
  AllocateNamed exclusively)
- source-archive evidence: derive the name via SanitizeMaterialName (so an
  empty --name yields "material-archive", not "input-archive"), make it
  collision-safe against existing materials, and cross-link in a single loop
- document that archive explode commits before the evidence step and is safe
  to re-run (deterministic names)
- replace selectorMatches with a shared nameMatches predicate
- test that an exploded first entry satisfies a required named contract slot

Assisted-by: Claude Code
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>

Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
Adds match_mode to PolicyGroup.Material so a policy group can target a set of
same-prefixed materials (e.g. an exploded archive), matching the standalone
PolicyAttachment selector. Both match sites now share the nameMatches
predicate, and InterpolateGroupMaterial carries match_mode through placeholder
interpolation. Default remains exact (UNSPECIFIED), so existing groups are
unchanged.

Assisted-by: Claude Code
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>

Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
Adds coverage across the explode matrix: uncompressed tar; --name with
multiple entries (name, name-1, name-2); --name colliding with an existing
material (derived names start at -1); max-total-size rollback; a mid-stage
staging failure rolling back every already-staged entry. Adds an end-to-end
test for the source-archive EVIDENCE cross-link (bidirectional references).

Assisted-by: Claude Code
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>

Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
…e tests

PREFIX now matches the exact name or a "<name>-<suffix>" derivative (the
explode naming scheme) rather than an arbitrary substring, so a selector
"scan" matches "scan" and "scan-1" but not "scanner". Adds edge cases:
prefix dash-boundary (standalone + group), an archive with no regular files,
and user-annotation propagation to every exploded material.

Assisted-by: Claude Code
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>

Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
Mirrors a multi-stage contract with two roles, each guarded by its own
name-PREFIX selector: exploding a role into <role>/<role>-1/… routes only that
role's policies, never the sibling role's, and unrelated names match neither.

Assisted-by: Claude Code
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>

Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
- use slices.Sorted(maps.Keys(...)) for the material-name helper (drops the
  hand-rolled sort + import)
- consolidate the two zip builders in the action test package: writeTestZip now
  delegates to writeZipWithFiles
- hoist the loop-invariant verifier in the multi-role selector test

Assisted-by: Claude Code
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>

Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
…e test

Replace substring Contains checks (trivially true) with an ElementsMatch on
the split reference list, so the archive's forward references must be exactly
the exploded material names.

Assisted-by: Claude Code
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>

Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
@chainloop-platform

Copy link
Copy Markdown
Contributor

AI Session Analysis

Missing AI Coding Sessions

We detected commits in this PR that were AI-assisted, but the matching Chainloop Trace session(s) could not be found in Chainloop.

Please make sure the AI coding session evidence has been sent by the Chainloop CLI, or add the skip-ai-session label to this PR to bypass this check.

Learn more about Chainloop Trace.


Powered by Chainloop and Chainloop Trace

@javirln
javirln marked this pull request as draft July 29, 2026 14:55

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 19 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread app/cli/pkg/action/attestation_add.go Outdated
Comment thread app/cli/pkg/action/attestation_add.go Outdated
javirln added 2 commits July 29, 2026 17:07
Resolves the archive.go conflict against upstream's archiveio refactor
(chainloop-dev#3310/chainloop-dev#3301): keep upstream's archiveio-backed primitives and re-apply the
AllocateNamed allocator and NormalizeArchivePath helper on top. Regenerated the
workflowcontract proto artifacts so the additive match_mode field coexists with
the upstream protovalidate bump.

Assisted-by: Claude Code
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>
…rning

Addresses code-review (cubic) findings:
- Fold the source-archive EVIDENCE material and its bidirectional cross-links
  into AddMaterialsFromArchive's single atomic commit, behind the new
  WithSourceArchiveEvidence option the CLI opts into. A failed evidence step now
  rolls the whole set back instead of leaving exploded materials that a retry
  would duplicate (the allocator seeds from existing names).
- Correct the --policy-input-from-file warning: the runtime inputs still apply
  to policy evaluation of every exploded material; only the per-input evidence
  materials are not recorded.

Assisted-by: Claude Code
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>

Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
@javirln
javirln marked this pull request as ready for review July 29, 2026 15:19

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread pkg/attestation/crafter/crafter.go Outdated
Comment thread app/controlplane/api/workflowcontract/v1/crafting_schema.proto Outdated
javirln added 2 commits July 30, 2026 08:05
…archive

Follow-up review (cubic): the reverse-edge assignment overwrote any
chainloop.material.references a caller set via runtimeAnnotations. Extract a
shared materials.AppendReferences that preserves existing entries and dedupes,
and route both the crafter reverse edge and the action addReference through it.

Assisted-by: Claude Code
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>

Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
…lity

Per review (Miguel): unprefixed values read better in contracts
(match_mode: PREFIX vs MATCH_MODE_PREFIX) and match the other unprefixed enums
in this file (RunnerType, MaterialType). ENUM_VALUE_PREFIX is already ignored
file-wide; the bare UNSPECIFIED zero value needs a single inline
buf:lint:ignore ENUM_ZERO_VALUE_SUFFIX. Regenerated pb.go/TS/jsonschema and
updated the Go references.

Assisted-by: Claude Code
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>

Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 10 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread app/controlplane/api/workflowcontract/v1/crafting_schema.proto
Comment thread pkg/policies/policies.go Outdated
@jiparis

jiparis commented Jul 30, 2026

Copy link
Copy Markdown
Member

Looks great, @javirln . Just a nit comment.

Per review (jiparis): a name-PREFIX selector should be a literal prefix, not
bounded on the "-" separator. The user provides the prefix, so silently
matching "<name>-" instead of "<name>" is inconsistent with what they wrote;
authors are expected to choose a discriminating prefix. Matching is now
strings.HasPrefix(materialID, name). Multi-role separation still holds because
distinct full-name prefixes do not cross-match.

Assisted-by: Claude Code
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>

Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
@javirln
javirln merged commit 8eed026 into chainloop-dev:main Jul 30, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants