Skip to content

Use a size annotation on the policy-evaluations bundle ref instead of calling the CAS in the workflow-run View API #3414

Closed

Description

@migmartri

Context

#3408 caps the policy-evaluations bundle the WorkflowRunService.View API is willing to download and inline. To learn the bundle size before paying for the transfer, the control plane calls CASClient.Describe against the org's CAS backend, and falls back to a REASON_UNAVAILABLE reference when the size cannot be determined.

This adds a CAS round trip (temporary credentials, gRPC connection, backend HEAD) on every View of an attestation that carries a policyEvaluationsRef, even when the bundle is small and would be inlined anyway. It also makes the decision depend on the backend reporting a content length, which some backends omit, in which case we refuse to inline a bundle that may be tiny.

Proposal

Record the bundle size in the attestation itself and use that instead of calling the CAS.

The CLI already builds the policyEvaluationsRef as an in-toto ResourceDescriptor in uploadPolicyEvaluationsBundle (app/cli/pkg/action/attestation_push.go). Today it only sets name, digest and mediaType. It has the marshalled bundle bytes in hand, so it can add the size, for example as chainloop.dev/size under annotations (or a dedicated field if we prefer). The descriptor is part of the signed statement, so the value is tamper-proof.

On the server side, resolvePolicyEvaluations (app/controlplane/internal/service/workflowrun.go) would then:

  1. Read the size from the descriptor. If it is above the cap, return REASON_TOO_LARGE without touching the CAS.
  2. If it is under the cap, download straight away, keeping the existing boundedBuffer so an inconsistent descriptor cannot push us past the cap.
  3. Only when the attestation predates the annotation (no size present) fall back to the current Describe call.

This removes the extra round trip from the hot path, works regardless of whether the backend reports a content length, and the result does not depend on CAS availability for the "too large" decision.

Notes

  • Existing attestations do not carry the annotation, so the Describe fallback needs to stay until we are comfortable dropping support for them.
  • Describe on biz.CASClient / casclient.Downloader can be kept for the fallback, or removed later together with it.
  • Related: the cap does not apply to orgs on the inline CAS backend, since the CLI skips the bundle upload there and the evaluations stay embedded in the predicate. That is a separate problem from this one.

🤖 Posted by Maximus bot (Claude Code) on behalf of @migmartri

Activity

  1. migmartri commented on Sep 8, 2026

    @migmartri
    MemberAuthor

    One more change to consider alongside this.

    Today AttestationItem.policy_evaluations_ref is only populated when the server declines to inline the evaluations (REASON_TOO_LARGE / REASON_UNAVAILABLE). When the evaluations are inlined, the field is empty and the caller has no way to learn the digest of the bundle from the API, even though the attestation carries it.

    I would return the reference always, whenever the attestation has a policyEvaluationsRef, regardless of whether the evaluations were inlined. The reason field would then express the outcome rather than only the failure:

    • REASON_UNSPECIFIED (or a new explicit REASON_INLINED): evaluations are present in policy_evaluations, the ref is informational.
    • REASON_TOO_LARGE / REASON_UNAVAILABLE: as today, policy_evaluations is empty and the caller must fetch the bundle.

    That gives consumers a stable pointer to the bundle (for download, caching, cross-referencing) independent of the server-side inlining decision, and it fits naturally with reading the size from the descriptor annotation proposed above, since size_bytes can then be filled from the attestation without a CAS call.

    The CLI workflow run describe mapping (pbPolicyEvaluationsRefToAction) currently treats an unspecified reason as unavailable, so it would need to be adjusted to not report "unavailable" when the evaluations are in fact inlined.

    🤖 Posted by Maximus bot (Claude Code) on behalf of @migmartri

  2. javirln commented on Sep 8, 2026

    @javirln
    Member

    Implemented in #3413 (draft).

    Against the ticket proposal

    • The size is recorded in the attestation. It went in as a dedicated predicate field, policyEvaluationsBundleSize, rather than a chainloop.dev/size descriptor annotation — the proposal allowed either. It sits alongside policyEvaluationsRef in the signed statement, so it is equally tamper-proof, and uploadPolicyEvaluationsBundle fills it from the bytes it has just marshalled.
    • resolvePolicyEvaluations gates on that size. Over the cap returns REASON_TOO_LARGE before even the CAS mapping lookup, so an oversized bundle now costs zero CAS calls. Under the cap it downloads straight away.
    • Describe is gone from the hot path and removed outright from biz.CASDownloader, casclient.Downloader and CASClientUseCase. The info.Size <= 0 refusal goes with it, so a backend that omits a content length no longer causes a small bundle to be reported unavailable.

    Two deliberate deviations, worth a second opinion

    1. The Describe fallback was not kept. Attestations with no recorded size are downloaded and inlined in full, uncapped. The cap therefore protects newly pushed attestations only, and existing runs — including the 134k-violation ones from PFM-7045 — go back to being inlined unbounded on View until they are re-attested. The ticket notes say the fallback should stay until we are comfortable dropping those; this PR drops them now.
    2. boundedBuffer was removed rather than kept for the under-cap download. Its purpose in fix(controlplane): cap the policy evaluations inlined by the workflow-run View API #3408 was to distrust a size that came from CAS metadata; with the size signed into the predicate the bound was judged redundant. The consequence is that a stored object that does not match its recorded size is read in full.

    Either can be restored if the exposure is not acceptable.

    Against the comment above

    Implemented as described. AttestationItem.policy_evaluations_ref is populated whenever the attestation carries a bundle, regardless of the inlining decision.

    One shape difference: instead of a REASON_INLINED enum value, the message gained bool inlined = 5 and reason stays strictly about failures, unset on the happy path. Same outcome for consumers, and it keeps a single field from carrying both success and failure semantics.

    size_bytes is filled without any CAS call in every case — the recorded size on the too-large path, the decoded byte count when inlined. pbPolicyEvaluationsRefToAction no longer reports "unavailable" for an unspecified reason when the evaluations are in fact inlined.

    Beyond the comment, workflow run describe prints a Policy evaluations bundle row carrying the chainloop artifact download --digest ... command wherever a digest is known — inlined, too large or unavailable alike.

    Not addressed

    The inline CAS backend case, which the ticket notes already flag as a separate problem.

    Drafted with Claude Code.

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions