Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Chainloop is an open-source evidence store for Software Supply Chain attestation

## Design Specs

Design specs live in `docs/specs/`. Each one records an intended design and its reasons. Read the relevant spec before you change that behavior. Cite requirement IDs (for example R-001) in PRs that implement them. When a change goes against an implemented spec, say so in the PR, and propose a new spec for a large change. Do not edit an implemented spec's design. Never put customer or user information in a spec. Parent tickets for specs live in the GitHub issues of this repository. Create and update specs with `br:eng-spec`, never by hand-rolling a new format.
Design specs live in `docs/specs/`. Each one records a design and its reasons at the time it was built. Before you change the behavior of an area, find it in the decisions index [`docs/decisions/README.md`](docs/decisions/README.md), if there is one. Then read its Current Decisions page. It shows the decisions and the words that hold today. `br:eng-adr` writes those pages: do not edit them by hand. Read the relevant spec for the reasons before you change that behavior. Cite requirement IDs (for example R-001) in PRs that implement them. While the ticket is in progress and the spec is on the default branch, supersede changed entries in the implementation PR. Sometimes the spec and its implementation are in the same PR, and the spec is not on the default branch yet. Then change the spec directly so that it matches the implementation. The agent instructions comment of the spec has the details. When a change goes against an implemented spec, say so in the PR, and propose a new spec for a large change. Do not edit an implemented spec's design. Never put customer or user information in a spec. Parent tickets for specs live in the GitHub issues of this repository. Create and update specs with `br:eng-spec`, never by hand-rolling a new format.

## Architecture

Expand Down
94 changes: 94 additions & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<!--
AGENT INSTRUCTIONS. Keep this comment. Read it before you change this file.

The br:eng-adr skill writes this page. Do not edit it by hand, and do not change it as part of other work. A person approves each change before the skill writes it, and the area owner reviews it in a PR.

Structure. The index (README.md) has the parts Areas (the area map), Relationships, Repository-wide decisions, Non-Confirmed, Superseded, and Open conflicts. An area page has the parts Language, Current decisions, Non-Confirmed, Superseded, and Open conflicts. Each decision is a "### <PFX>-NNN: <title>" entry with the choice, then the lines Why, Rejected, Deciders, Source, and Code.
- Current decisions and Repository-wide decisions: the rules that hold today. Follow them. The page holds only choices that a reader of the code would likely get wrong. It does not describe how the system works: for that, read the code or the specs.
- Non-Confirmed: decisions that the code does not show yet, for example a plan in a spec. They are not rules yet.
- Superseded: old decisions, struck through, each with the ID of the decision that replaced it. They are history only.
- Open conflicts: drift and disagreements that a person must settle.
- Language: the words of the area, and the words to avoid.

- Before you change the behavior of an area, read its current decisions and its Language. The page leaves out smaller decisions (the "Not shown" line), so also check the Decision Records of the specs that the page lists. Read the linked spec for the reasons.
- To record a decision that the team made and built: run br:eng-adr new.
- To change a decision, or to make one that is not built yet: write a spec with br:eng-spec. Its Decision Record entry says "replaces <PFX>-NNN".
- To refresh this page from the specs and the code: run br:eng-adr reconcile.
- Never renumber or reuse a decision ID. Never delete a decision: supersede it.
- When your code goes against a current decision, say so in your PR. Do not edit this page to match the code.
-->

# Current Decisions

> Written by `eng-adr`. To change a decision, write a spec or run `eng-adr new`. Do not edit this page by hand.

Last refresh: 2026-11-01, commit 4e150ca7.

## Areas

### Artifact CAS
- **Page:** [artifact-cas.md](artifact-cas.md)
- **Prefix:** CAS
- **Owner:** migmartri
- **Purpose:** Content-addressable storage of evidence blobs, with its upload and download paths and its caches.
- **Does not own:** the listeners, ports, TLS, and ingress of the CAS (Service Operations), and the download tokens that the control plane makes (Authentication).
- **Specs:** [issue-3543-cas-existence-cache](../specs/issue-3543-cas-existence-cache.md), [issue-3549-cas-download-browser-cache](../specs/issue-3549-cas-download-browser-cache.md)
- **Code:** `app/artifact-cas/`, `pkg/blobmanager/`, `pkg/cache/casexistence/`, `pkg/middlewares/http/`

### Attestation Process
- **Page:** [attestation-process.md](attestation-process.md)
- **Prefix:** ATT
- **Owner:** migmartri
- **Purpose:** The attestation lifecycle of the CLI (`init`, `add`, `status`, `push`, `reset`, `verify`), and how it selects its organization and project.
- **Does not own:** AI coding session evidence (Chainloop Trace), and login and tokens (Authentication).
- **Specs:** [issue-3063-attestation-repo-config](../specs/issue-3063-attestation-repo-config.md)
- **Code:** `app/cli/cmd/attestation*.go`, `app/cli/pkg/action/attestation_*.go`, `app/cli/internal/repositoryconfig/`, `pkg/attestation/`

### Authentication
- **Page:** [authentication.md](authentication.md)
- **Prefix:** AUT
- **Owner:** migmartri
- **Purpose:** User login through the control plane, the login callback, and the hand-off of the session token to the CLI and other clients.
- **Does not own:** the CAS download token header (Artifact CAS), and the organization that a command uses (Attestation Process).
- **Specs:** [issue-3626-login-code-exchange](../specs/issue-3626-login-code-exchange.md)
- **Code:** `app/controlplane/internal/service/auth.go`, `app/controlplane/pkg/biz/loginhandoff.go`, `app/controlplane/pkg/data/loginhandoff.go`, `app/cli/cmd/auth_login.go`

### Service Operations
- **Page:** [service-operations.md](service-operations.md)
- **Prefix:** OPS
- **Owner:** migmartri
- **Purpose:** The network endpoints of the control plane and the CAS: listeners, protocols, TLS, internal ports, and the Helm chart ingress.
- **Does not own:** what the endpoints serve (Artifact CAS, Authentication).
- **Specs:** [issue-3628-single-endpoint](../specs/issue-3628-single-endpoint.md), [issue-3628-controlplane-http-dispatch](../specs/issue-3628-controlplane-http-dispatch.md)
- **Code:** `app/controlplane/internal/server/`, `app/artifact-cas/internal/server/`, `deployment/chainloop/`

### Chainloop Trace
- **Page:** [chainloop-trace.md](chainloop-trace.md)
- **Prefix:** TRC
- **Owner:** jiparis
- **Purpose:** Recording AI coding sessions as evidence: providers, spec and skill capture, transcript pointers, line attribution, local mode, and the e2e test matrix.
- **Does not own:** the attestation lifecycle and the organization order of `.chainloop.yml` (Attestation Process), and blob storage (Artifact CAS).
- **Specs:** [issue-3495-session-spec-capture](../specs/issue-3495-session-spec-capture.md), [issue-3515-spec-capture-during-session](../specs/issue-3515-spec-capture-during-session.md), [issue-3520-pi-trace-provider](../specs/issue-3520-pi-trace-provider.md), [issue-3531-spec-source-role](../specs/issue-3531-spec-source-role.md), [issue-3540-trace-skills-used](../specs/issue-3540-trace-skills-used.md), [issue-3556-session-image-placeholders](../specs/issue-3556-session-image-placeholders.md), [issue-3561-up-to-date-local-spec-evidence](../specs/issue-3561-up-to-date-local-spec-evidence.md), [issue-3569-pasted-image-pointers](../specs/issue-3569-pasted-image-pointers.md), [issue-3582-trace-evidence-export](../specs/issue-3582-trace-evidence-export.md), [issue-3635-trace-e2e-matrix](../specs/issue-3635-trace-e2e-matrix.md), [issue-3639-line-ranges](../specs/issue-3639-line-ranges.md), [issue-3639-trace-line-attribution](../specs/issue-3639-trace-line-attribution.md), [issue-3657-codex-trace-provider](../specs/issue-3657-codex-trace-provider.md)
- **Code:** `app/cli/internal/trace/`, `app/cli/cmd/trace*.go`, `app/cli/pkg/action/trace_*.go`, `pkg/attestation/crafter/materials/aicodingsession/`

## Relationships

- **Attestation Process → Chainloop Trace:** both read `.chainloop.yml` through one reader (ATT-003, attestation-process.md). A change to the file order or keys changes both areas.
- **Chainloop Trace → Artifact CAS:** On a push, Trace uploads each spec source, skill package, and pasted image as a material. Local mode uses neither. A transcript pointer replaces a copy only when its material is stored (TRC-016, chainloop-trace.md).
- **Service Operations → Artifact CAS, Authentication:** the shared endpoints carry the CAS ByteStream and download routes and the control plane login endpoints. A change to their dispatch affects all of them. It does not affect ByteStream traffic on the legacy gRPC listener of the CAS. That listener is on by default, and the chart sends the control plane connection to the CAS to it. The chart sets the TLS of both listeners from one secret.

## Repository-wide decisions

None.

## Non-Confirmed

None.

## Superseded

None.

## Open conflicts
Comment thread
migmartri marked this conversation as resolved.
Resolved

None.
117 changes: 117 additions & 0 deletions docs/decisions/artifact-cas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<!--
AGENT INSTRUCTIONS. Keep this comment. Read it before you change this file.

The br:eng-adr skill writes this page. Do not edit it by hand, and do not change it as part of other work. A person approves each change before the skill writes it, and the area owner reviews it in a PR.

Structure. The index (README.md) has the parts Areas (the area map), Relationships, Repository-wide decisions, Non-Confirmed, Superseded, and Open conflicts. An area page has the parts Language, Current decisions, Non-Confirmed, Superseded, and Open conflicts. Each decision is a "### <PFX>-NNN: <title>" entry with the choice, then the lines Why, Rejected, Deciders, Source, and Code.
- Current decisions and Repository-wide decisions: the rules that hold today. Follow them. The page holds only choices that a reader of the code would likely get wrong. It does not describe how the system works: for that, read the code or the specs.
- Non-Confirmed: decisions that the code does not show yet, for example a plan in a spec. They are not rules yet.
- Superseded: old decisions, struck through, each with the ID of the decision that replaced it. They are history only.
- Open conflicts: drift and disagreements that a person must settle.
- Language: the words of the area, and the words to avoid.

- Before you change the behavior of an area, read its current decisions and its Language. The page leaves out smaller decisions (the "Not shown" line), so also check the Decision Records of the specs that the page lists. Read the linked spec for the reasons.
- To record a decision that the team made and built: run br:eng-adr new.
- To change a decision, or to make one that is not built yet: write a spec with br:eng-spec. Its Decision Record entry says "replaces <PFX>-NNN".
- To refresh this page from the specs and the code: run br:eng-adr reconcile.
- Never renumber or reuse a decision ID. Never delete a decision: supersede it.
- When your code goes against a current decision, say so in your PR. Do not edit this page to match the code.
-->

# Artifact CAS: Current Decisions

> Written by `eng-adr`. To change a decision, write a spec or run `eng-adr new`. Do not edit this page by hand.

**Purpose:** Content-addressable storage of evidence blobs, with its upload and download paths and its caches. **Owner:** migmartri. **Does not own:** the listeners, ports, TLS, and ingress of the CAS (Service Operations), and the download tokens that the control plane makes (Authentication). **Specs:** [issue-3543-cas-existence-cache](../specs/issue-3543-cas-existence-cache.md), [issue-3549-cas-download-browser-cache](../specs/issue-3549-cas-download-browser-cache.md). **Last refresh:** 2026-11-01, commit 4e150ca7.

## Language

- **Existence cache**: the server-side cache of "this blob is in the backend" results, keyed per organization and backend. _Avoid_: "upload cache", because the cache holds no content.
- **Client cache**: the in-process cache of backend clients and their credentials. _Avoid_: "credentials cache", because the credentials are part of the client and never stored alone.

## Current decisions

### CAS-001: Existence cache in NATS KV when connected, in memory otherwise
The existence cache uses the shared cache library. It uses NATS KV when the CAS has a NATS connection. It uses an in-memory LRU when it has no connection, or when the NATS bucket cannot be set up at startup. A NATS failure at runtime is a cache miss, not a change to memory.
- Why: replicas share hits when NATS is there, and the CAS gets no new dependency when it is not.
- Rejected: memory only (the hit rate drops as replicas grow). NATS required (a hard dependency for a speed gain).
- Deciders: migmartri, javirln
- Source: [issue-3543-cas-existence-cache D-001](../specs/issue-3543-cas-existence-cache.md)
- Code: verified: `pkg/cache/casexistence/casexistence.go`, `app/artifact-cas/cmd/wire.go`.

### CAS-002: The existence cache lives in the CAS, server side
The CAS ByteStream write checks the cache and answers "already exists" with no backend call. The CLI has no cache.
- Why: it works for all CLI versions and all clients, with no API change.
- Rejected: a cache in the CLI (each command is a new process, so nothing is reused).
- Deciders: migmartri, javirln
- Source: [issue-3543-cas-existence-cache D-003](../specs/issue-3543-cas-existence-cache.md)
- Code: verified: `app/artifact-cas/internal/service/bytestream.go`, `app/artifact-cas/internal/service/cache.go`.

### CAS-003: The cache key starts with the organization
The key is the organization, then the backend type, then a hash of the secret reference, then the digest. The cache has its own bucket.
- Why: the organization prefix keeps the entries of one tenant away from all other tenants, also on shared storage. The secret reference separates backends in one organization.
- Rejected: the digest alone (the upload of one tenant answers for another tenant, and shows that the other tenant has the file). The backend and the digest without the organization (a shared backend leaks the same information).
- Deciders: migmartri, javirln
- Source: [issue-3543-cas-existence-cache D-004](../specs/issue-3543-cas-existence-cache.md)
- Code: verified: `pkg/cache/casexistence/casexistence.go`. `app/artifact-cas/internal/service/cache.go` skips the cache when the organization is empty.

### CAS-004: Credentials never go to a shared store
The client cache, which holds backend credentials, stays in process memory. It never uses NATS.
- Why: credentials must not leave the process.
- Deciders: migmartri, javirln
- Source: [issue-3543-cas-existence-cache D-006](../specs/issue-3543-cas-existence-cache.md)
- Code: verified: `app/artifact-cas/internal/service/cache.go` (in-process expirable LRU).

### CAS-005: Downloads use the browser HTTP cache
The HTTP download endpoint of the CAS sets cache headers and answers 304 when the browser has the content. ByteStream downloads do not change. The control plane keeps no copy of the bytes.
- Why: the browser downloads straight from the CAS, so a cache there skips the backend copy and the transfer.
- Rejected: a server cache in the control plane (the control plane never sees the bytes, and its message size limit is too small).
- Deciders: jiparis, migmartri
- Source: [issue-3549-cas-download-browser-cache D-001](../specs/issue-3549-cas-download-browser-cache.md)
- Code: verified: `app/artifact-cas/internal/service/download.go`.

### CAS-006: The download token goes in the Authorization header
The CAS accepts the download token in an `Authorization: Bearer` header. When a client moves the token from the `?t=` query to this header, the download URL stays the same between calls.
- Why: the browser cache uses the URL as its key, and the token changes on each call.
- Rejected: one token for each user and digest (the URL stays the same only for the life of the token).
- Deciders: jiparis, migmartri
- Source: [issue-3549-cas-download-browser-cache D-003](../specs/issue-3549-cas-download-browser-cache.md)
- Code: verified on the CAS side: `pkg/middlewares/http/jwt.go`. When `cors.allow_origins` is set, the preflight in `app/artifact-cas/internal/server/cors.go` allows `Authorization`. The control plane `GetDownloadURL` still puts the token in `?t=` (`app/controlplane/internal/service/casredirect.go`). The client that moves the token to the header is not in this repository.

### CAS-007: The header token wins, with no fallback to the query
When a request has a token in the header and in the query, the CAS uses only the header. A bad header fails with 401.
- Why: a client that sends a header sets its intent.
- Rejected: reject a request with both (the viewer must then strip the query). The query wins (the header has no effect for a URL that still holds a token).
- Deciders: jiparis, migmartri
- Source: [issue-3549-cas-download-browser-cache D-004](../specs/issue-3549-cas-download-browser-cache.md)
- Code: verified: `pkg/middlewares/http/jwt.go`.

### CAS-008: The CAS checks that the object exists before a 304
Before it answers 304, the CAS makes a metadata call to the backend. A deleted object gives 404.
- Why: a deleted object stops showing at once, and the metadata call is cheap.
- Rejected: a 304 from the digest alone (a browser can show a deleted object until it drops its copy).
- Deciders: jiparis, migmartri
- Source: [issue-3549-cas-download-browser-cache D-005](../specs/issue-3549-cas-download-browser-cache.md)
- Code: verified: `app/artifact-cas/internal/service/download.go`.

### CAS-009: The existence cache lifetime is fixed, with no off switch
Replaces [issue-3543-cas-existence-cache D-007](../specs/issue-3543-cas-existence-cache.md) and the settings of R-004. An existence entry expires after 24 hours. No setting changes the lifetime or turns the cache off.
- Why: blobs cannot be deleted through Chainloop, so a fixed 24-hour bound covers the only risk. A setting adds configuration that no operator needs.
- Rejected: a configurable lifetime and an operator switch that turns the cache off (spec D-007 and R-004).
- Deciders: migmartri
- Source: recorded directly, 2026-11-03
- Code: verified: `pkg/cache/casexistence/casexistence.go` (`ttl` constant), `app/artifact-cas/cmd/wire.go` (the cache is always on).

Not shown: 5 smaller or replaced decisions of [issue-3543-cas-existence-cache](../specs/issue-3543-cas-existence-cache.md), 2 of [issue-3549-cas-download-browser-cache](../specs/issue-3549-cas-download-browser-cache.md).
Comment thread
migmartri marked this conversation as resolved.
Resolved

## Non-Confirmed

None.

## Superseded

None.

## Open conflicts

None.
Loading
Loading