Two release tags are not ancestors of main, and two published images have no tag at all #14

Open
opened 2026-08-27 02:09:09 +00:00 by jlxq0 · 6 comments
Owner

Two release tags are not ancestors of main, two published images have no git tag at all, and nothing in CI checks either. Filed because a branch has no state and nothing will otherwise remind anyone.

The measurement

branch ahead behind tip tag on tip
feat/claude-redirects 1 39 125f72e none
fix/rfc8707-absolute-resource 2 40 cf08af4 v0.2.8
fix/stalwart-jmap-audience 1 40 b5b1f41 v0.2.7

Tag ancestry against main:

v0.1.0 .. v0.2.4   on-main = yes
v0.2.7             on-main = NO   commit b5b1f41
v0.2.8             on-main = NO   commit cf08af4
v0.2.9 .. v0.2.14  on-main = yes

v0.2.7 and v0.2.8 are annotated tags, so git rev-parse v0.2.7 returns the tag object 2deb270, not the commit b5b1f41. Compare with ^{} or the ancestry answer is about the wrong object.

Two more versions exist as images with no tag in either the local clone or the forge API. Both were built, both survive, and the tags that produced them are gone:

jmap-mcp:v0.2.5   image created 2026-08-16T01:35:18Z   no git tag
jmap-mcp:v0.2.6   image created 2026-08-16T06:30:23Z   no git tag

So the registry holds v0.2.5 through v0.2.8 while main's history accounts for none of the four. GHCR holds only 0.2.10 through 0.2.14, so none of them ever reached the public mirror.

Is a shipped fix missing from the current line?

No, and the work was superseded rather than lost. The two branch commits and main were solving the same 2026-08 InvalidAudience outage, and main fixed it further upstream.

b5b1f41 (v0.2.7) forced resource=stalwart on the OAuth proxy and widened inbound validation to accept origin, origin/mcp and stalwart. cf08af4 (v0.2.8) then reverted the core of that a half hour later, because Logto rejects a bare API name under RFC 8707 with invalid_target, and sent the origin instead.

main reached the same destination through 8808b09, which canonicalises JMAP_MCP_RESOURCE_URL to the bare origin so every derived URL follows, plus normalize_resource in src/oauth_proxy.rs:143, which strips a trailing / and a trailing /mcp from the RFC 8707 indicator before proxying. For the deployed configuration both designs put the same value on the wire. main's has one source of truth instead of a second env var.

Observed on the live service rather than inferred from the source:

RFC 9728 resource      https://jmap-mcp.kampong.social/mcp
RFC 8414 issuer        https://jmap-mcp.kampong.social
authorization_endpoint https://jmap-mcp.kampong.social/authorize
token_endpoint         https://jmap-mcp.kampong.social/token
401 WWW-Authenticate   resource_metadata=".../.well-known/oauth-protected-resource/mcp"

That is exactly the contract 8808b09 documents: advertised resource is <origin>/mcp, JWT audience and RFC 8414 issuer are the bare origin.

STALWART_AUDIENCE is not orphaned config. It appears in src/config.rs and src/main.rs on the branch and nowhere on main, and the live Deployment does not set it. Every JMAP_MCP_* variable the manifest sets is one main reads, so nothing is being silently ignored.

One real narrowing, stated precisely

The branch carries expected_audiences: Vec<String>; main carries expected_audience: String (src/logto_oidc.rs:112). main therefore accepts exactly one audience where the branch accepted several.

This is not a regression on the deployed path, because main normalises the outgoing resource so Logto only ever mints origin-aud tokens. It is a narrowing for any token minted against Logto without passing through this proxy. Recording it rather than calling it equivalent.

feat/claude-redirects is fully superseded

Its single commit changes only the fixture string in the deployed_allowlist_parses test. main already carries the identical nine entries at src/oauth_redirect.rs:281-283, including claude://claude.ai/oauth/callback, claude://oauth/callback and cowork://oauth/callback, with the same assert_eq!(allowed.len(), 9). Nothing is lost by deleting this branch.

How a tag got cut off an unmerged branch

Nothing prevents it, and the workflow states the assumption it does not enforce. .forgejo/workflows/ci.yml:130 publishes on any v* tag:

if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
  V="${GITHUB_REF#refs/tags/}"
  test "${V#v}" = "$VERSION"

The only gate is that the tag matches Cargo.toml's version. Ancestry is never consulted: merge-base, is-ancestor and refs/heads/main appear nowhere in the workflow except an unrelated cache branch. Four lines below, at ci.yml:135:

# Import only. A tag build is the same commit as the main build that preceded it, so its cache would be byte-identical

That assumption is what v0.2.7 and v0.2.8 violate. It is load-bearing for the buildcache reasoning and unenforced, which is the class of defect this repo keeps finding: the check that would have caught it does not exist, and every signal that does exist stayed green.

v0.2.5 and v0.2.6 show the second half. A tag can be deleted after its image is published, leaving an artefact whose source commit is unresolvable from the repository.

Proposed

  • Delete feat/claude-redirects. Established above as superseded.
  • Decide fix/rfc8707-absolute-resource and fix/stalwart-jmap-audience. On the evidence here both are superseded, but they carry the only commits behind two published images and should not be deleted on one session's reading.
  • Gate the tag build on ancestry: fail the run when the tagged commit is not an ancestor of main. This is also what makes the ci.yml:135 cache assumption true rather than hoped for.
  • Decide what v0.2.5 and v0.2.6 are. Either recover which commit built them or delete the images, since an artefact with no resolvable source is worse than no artefact.

Job logs are not retrievable through this Forgejo instance, so which run published the v0.2.7 and v0.2.8 images cannot be established from the API. The task list shows only failed cargo runs for both refs, and the images exist, so a re-run is the likely explanation and is not evidence I have.

Two release tags are not ancestors of `main`, two published images have no git tag at all, and nothing in CI checks either. Filed because a branch has no state and nothing will otherwise remind anyone. ## The measurement | branch | ahead | behind | tip | tag on tip | |---|---|---|---|---| | `feat/claude-redirects` | 1 | 39 | `125f72e` | none | | `fix/rfc8707-absolute-resource` | 2 | 40 | `cf08af4` | `v0.2.8` | | `fix/stalwart-jmap-audience` | 1 | 40 | `b5b1f41` | `v0.2.7` | Tag ancestry against `main`: ``` v0.1.0 .. v0.2.4 on-main = yes v0.2.7 on-main = NO commit b5b1f41 v0.2.8 on-main = NO commit cf08af4 v0.2.9 .. v0.2.14 on-main = yes ``` `v0.2.7` and `v0.2.8` are annotated tags, so `git rev-parse v0.2.7` returns the tag object `2deb270`, not the commit `b5b1f41`. Compare with `^{}` or the ancestry answer is about the wrong object. **Two more versions exist as images with no tag in either the local clone or the forge API.** Both were built, both survive, and the tags that produced them are gone: ``` jmap-mcp:v0.2.5 image created 2026-08-16T01:35:18Z no git tag jmap-mcp:v0.2.6 image created 2026-08-16T06:30:23Z no git tag ``` So the registry holds `v0.2.5` through `v0.2.8` while `main`'s history accounts for none of the four. GHCR holds only `0.2.10` through `0.2.14`, so none of them ever reached the public mirror. ## Is a shipped fix missing from the current line? **No, and the work was superseded rather than lost.** The two branch commits and `main` were solving the same 2026-08 `InvalidAudience` outage, and `main` fixed it further upstream. `b5b1f41` (v0.2.7) forced `resource=stalwart` on the OAuth proxy and widened inbound validation to accept `origin`, `origin/mcp` and `stalwart`. `cf08af4` (v0.2.8) then reverted the core of that a half hour later, because Logto rejects a bare API name under RFC 8707 with `invalid_target`, and sent the origin instead. `main` reached the same destination through `8808b09`, which canonicalises `JMAP_MCP_RESOURCE_URL` to the bare origin so every derived URL follows, plus `normalize_resource` in `src/oauth_proxy.rs:143`, which strips a trailing `/` and a trailing `/mcp` from the RFC 8707 indicator before proxying. For the deployed configuration both designs put the same value on the wire. `main`'s has one source of truth instead of a second env var. Observed on the live service rather than inferred from the source: ``` RFC 9728 resource https://jmap-mcp.kampong.social/mcp RFC 8414 issuer https://jmap-mcp.kampong.social authorization_endpoint https://jmap-mcp.kampong.social/authorize token_endpoint https://jmap-mcp.kampong.social/token 401 WWW-Authenticate resource_metadata=".../.well-known/oauth-protected-resource/mcp" ``` That is exactly the contract `8808b09` documents: advertised resource is `<origin>/mcp`, JWT audience and RFC 8414 issuer are the bare origin. **`STALWART_AUDIENCE` is not orphaned config.** It appears in `src/config.rs` and `src/main.rs` on the branch and nowhere on `main`, and the live Deployment does not set it. Every `JMAP_MCP_*` variable the manifest sets is one `main` reads, so nothing is being silently ignored. ### One real narrowing, stated precisely The branch carries `expected_audiences: Vec<String>`; `main` carries `expected_audience: String` (`src/logto_oidc.rs:112`). `main` therefore accepts exactly one audience where the branch accepted several. This is not a regression on the deployed path, because `main` normalises the outgoing `resource` so Logto only ever mints origin-`aud` tokens. It is a narrowing for any token minted against Logto without passing through this proxy. Recording it rather than calling it equivalent. ### `feat/claude-redirects` is fully superseded Its single commit changes only the fixture string in the `deployed_allowlist_parses` test. `main` already carries the identical nine entries at `src/oauth_redirect.rs:281-283`, including `claude://claude.ai/oauth/callback`, `claude://oauth/callback` and `cowork://oauth/callback`, with the same `assert_eq!(allowed.len(), 9)`. Nothing is lost by deleting this branch. ## How a tag got cut off an unmerged branch Nothing prevents it, and the workflow states the assumption it does not enforce. `.forgejo/workflows/ci.yml:130` publishes on any `v*` tag: ``` if [[ "$GITHUB_REF" == refs/tags/v* ]]; then V="${GITHUB_REF#refs/tags/}" test "${V#v}" = "$VERSION" ``` The only gate is that the tag matches `Cargo.toml`'s version. Ancestry is never consulted: `merge-base`, `is-ancestor` and `refs/heads/main` appear nowhere in the workflow except an unrelated cache branch. Four lines below, at `ci.yml:135`: > `# Import only. A tag build is the same commit as the `main` build that preceded it, so its cache would be byte-identical` That assumption is what `v0.2.7` and `v0.2.8` violate. It is load-bearing for the buildcache reasoning and unenforced, which is the class of defect this repo keeps finding: the check that would have caught it does not exist, and every signal that does exist stayed green. `v0.2.5` and `v0.2.6` show the second half. A tag can be deleted after its image is published, leaving an artefact whose source commit is unresolvable from the repository. ## Proposed - [ ] Delete `feat/claude-redirects`. Established above as superseded. - [ ] Decide `fix/rfc8707-absolute-resource` and `fix/stalwart-jmap-audience`. On the evidence here both are superseded, but they carry the only commits behind two published images and should not be deleted on one session's reading. - [ ] Gate the tag build on ancestry: fail the run when the tagged commit is not an ancestor of `main`. This is also what makes the `ci.yml:135` cache assumption true rather than hoped for. - [ ] Decide what `v0.2.5` and `v0.2.6` are. Either recover which commit built them or delete the images, since an artefact with no resolvable source is worse than no artefact. Job logs are not retrievable through this Forgejo instance, so which run published the `v0.2.7` and `v0.2.8` images cannot be established from the API. The task list shows only failed `cargo` runs for both refs, and the images exist, so a re-run is the likely explanation and is not evidence I have.
Author
Owner

feat/claude-redirects deleted at 0e73d2f. Its tip was 125f72e, recorded above, so the commit is named here if it is ever wanted. The two tagged branches are untouched pending an answer on this issue.

The mergeable observation from arming branch protection is recorded in AGENTS.md rather than here, since it is a permanent property of the API and not something this issue resolves: PR #15, merged at 0e73d2f.

`feat/claude-redirects` deleted at `0e73d2f`. Its tip was `125f72e`, recorded above, so the commit is named here if it is ever wanted. The two tagged branches are untouched pending an answer on this issue. The `mergeable` observation from arming branch protection is recorded in `AGENTS.md` rather than here, since it is a permanent property of the API and not something this issue resolves: PR #15, merged at `0e73d2f`.
Author
Owner

State, 2026-08-27

Two of the four proposals are done and two remain. Recording it here because the issue body still reads as if none were.

Done

  • feat/claude-redirects deleted at 0e73d2f, tip 125f72e recorded above first. Superseded: its only change was a test fixture main already carried verbatim.
  • fix/rfc8707-absolute-resource and fix/stalwart-jmap-audience deleted. Both tags survive the deletion because they are annotated: v0.2.7 still resolves to b5b1f41 and v0.2.8 to cf08af4.
  • Ancestry gate shipped in v0.2.15 (PR #16). tag-ancestry runs on every event and docker needs it, in both pipelines. Proven both directions on the real runner: an off-main tag gave tag-ancestry=failure with cargo=success beside it and no docker task and no image; v0.2.15 and v0.2.16 both gave tag-ancestry=success and published.

Still open

  • v0.2.5 and v0.2.6 are images with no git tag and no resolvable source commit. Created 2026-08-16T01:35:18Z and 06:30:23Z. Neither the local clone nor the forge API has a tag for either, so a tag was deleted after its image was published.

    The ancestry gate cannot reach this and never could. It fires on a tag push; these have no tag to fire on. Recording that as the gate’s limit rather than as a reason to close the issue.

    Either the source commit is recovered or the images are deleted. An artefact with no resolvable source is worse than no artefact, and it is worse than the tags-off-unmerged-branches case this issue opened with, because there at least the commits exist.

The audience narrowing (expected_audiences: Vec to expected_audience: String) stays a paragraph rather than an item, on the reasoning above: not a regression on the deployed path. If anything ever mints a token against Logto without passing through this proxy, it becomes an item then.

Separately resolved and not part of this issue: JMAP_MCP_TRUSTED_PROXY_HOPS is now 2, measured, shipped in v0.2.16.

## State, 2026-08-27 Two of the four proposals are done and two remain. Recording it here because the issue body still reads as if none were. **Done** - [x] `feat/claude-redirects` deleted at `0e73d2f`, tip `125f72e` recorded above first. Superseded: its only change was a test fixture `main` already carried verbatim. - [x] `fix/rfc8707-absolute-resource` and `fix/stalwart-jmap-audience` deleted. Both tags survive the deletion because they are annotated: `v0.2.7` still resolves to `b5b1f41` and `v0.2.8` to `cf08af4`. - [x] Ancestry gate shipped in **v0.2.15** (PR #16). `tag-ancestry` runs on every event and `docker` needs it, in both pipelines. Proven both directions on the real runner: an off-main tag gave `tag-ancestry=failure` with `cargo=success` beside it and no docker task and no image; `v0.2.15` and `v0.2.16` both gave `tag-ancestry=success` and published. **Still open** - [ ] **`v0.2.5` and `v0.2.6` are images with no git tag and no resolvable source commit.** Created 2026-08-16T01:35:18Z and 06:30:23Z. Neither the local clone nor the forge API has a tag for either, so a tag was deleted after its image was published. **The ancestry gate cannot reach this and never could.** It fires on a tag push; these have no tag to fire on. Recording that as the gate’s limit rather than as a reason to close the issue. Either the source commit is recovered or the images are deleted. An artefact with no resolvable source is worse than no artefact, and it is worse than the tags-off-unmerged-branches case this issue opened with, because there at least the commits exist. The audience narrowing (`expected_audiences: Vec` to `expected_audience: String`) stays a paragraph rather than an item, on the reasoning above: not a regression on the deployed path. If anything ever mints a token against Logto without passing through this proxy, it becomes an item then. Separately resolved and not part of this issue: `JMAP_MCP_TRUSTED_PROXY_HOPS` is now 2, measured, shipped in v0.2.16.
Author
Owner

What still resolves v0.2.5 and v0.2.6, measured 2026-09-01

Establishing this and nothing more. The images are untouched and deleting them is a separate decision.

v0.2.5   sha256:bd58cf3d952876df55ad4c812ec768299be98dd9a671bc72aef5454e7d9f1378
v0.2.6   sha256:b876e85aba722a442d2f8b4d6908d3285c1d64ae80efc59f0227b5c442706166

Nothing references either

checked result
oddie-apps/platform, every jmap/fondue path in a 903-blob tree, for both digests and for jmap-mcp:v0.2.5/v0.2.6 0 files
running pods, all namespaces, 513 container statuses 0 on either digest
the same scan, positive control 1 jmap-mcp container found, on 196ee90a… = v0.2.21
forge registry latest 196ee90a…, so not pointing at either
GHCR version tags 0.2.10 0.2 latest 0.2.11 0.2.12 0.2.13 0.2.14 0.2.19 0.2.20 0.2.21neither 0.2.5 nor 0.2.6
forge registry still carries both

The positive control matters: a scan that finds nothing and a scan pointed at the wrong place produce the same output, so the 1 jmap-mcp container it did find is what makes the two zeroes readable.

So: the forge registry is the only thing holding them. No manifest, no GitOps path, no pod, no public mirror, and latest does not resolve to either.

One new corroboration of the local-build finding

Both are single manifests (application/vnd.docker.distribution.manifest.v2+json), while every CI-built release in this repository is an application/vnd.oci.image.index.v1+json. That is a third independent signal alongside the absent OCI labels and the absent Actions run, and it points the same way: these did not come out of the buildctl path in .forgejo/workflows/ci.yml.

The finding that generalises

A gate inside the pipeline cannot see an artefact that arrived outside it. The tag-ancestry gate added in v0.2.15 closes tags cut off unmerged branches and closes nothing here, because it runs in CI and this path never entered CI. Anything holding registry push credentials can publish, and the result is indistinguishable in the registry from a released image.

Still open

Whether to delete them. On the evidence above nothing breaks, but an artefact with no resolvable source is a different question from an artefact nobody uses, and deleting published images is destructive.

## What still resolves `v0.2.5` and `v0.2.6`, measured 2026-09-01 Establishing this and nothing more. **The images are untouched and deleting them is a separate decision.** v0.2.5 sha256:bd58cf3d952876df55ad4c812ec768299be98dd9a671bc72aef5454e7d9f1378 v0.2.6 sha256:b876e85aba722a442d2f8b4d6908d3285c1d64ae80efc59f0227b5c442706166 ### Nothing references either | checked | result | |---|---| | `oddie-apps/platform`, every `jmap`/`fondue` path in a 903-blob tree, for both digests and for `jmap-mcp:v0.2.5`/`v0.2.6` | **0 files** | | running pods, all namespaces, 513 container statuses | **0 on either digest** | | the same scan, positive control | **1** jmap-mcp container found, on `196ee90a…` = v0.2.21 | | forge registry `latest` | `196ee90a…`, so not pointing at either | | GHCR version tags | `0.2.10 0.2 latest 0.2.11 0.2.12 0.2.13 0.2.14 0.2.19 0.2.20 0.2.21` — **neither 0.2.5 nor 0.2.6** | | forge registry | **still carries both** | The positive control matters: a scan that finds nothing and a scan pointed at the wrong place produce the same output, so the 1 jmap-mcp container it did find is what makes the two zeroes readable. So: **the forge registry is the only thing holding them.** No manifest, no GitOps path, no pod, no public mirror, and `latest` does not resolve to either. ### One new corroboration of the local-build finding Both are **single manifests** (`application/vnd.docker.distribution.manifest.v2+json`), while every CI-built release in this repository is an `application/vnd.oci.image.index.v1+json`. That is a third independent signal alongside the absent OCI labels and the absent Actions run, and it points the same way: these did not come out of the `buildctl` path in `.forgejo/workflows/ci.yml`. ### The finding that generalises **A gate inside the pipeline cannot see an artefact that arrived outside it.** The tag-ancestry gate added in v0.2.15 closes tags cut off unmerged branches and closes nothing here, because it runs in CI and this path never entered CI. Anything holding registry push credentials can publish, and the result is indistinguishable in the registry from a released image. ### Still open Whether to delete them. On the evidence above nothing breaks, but an artefact with no resolvable source is a different question from an artefact nobody uses, and deleting published images is destructive.
Author
Owner

Why this stays open after the reference scan

The scan above closes the usage half and settles nothing about the other one.

Nothing breaks if the two images go, but an artefact with no resolvable source is a different question from an artefact nobody uses, and only the second is what was measured. Two zeroes and a positive control establish that no manifest, no GitOps path, no pod and no public mirror resolves either digest. They say nothing about which commit produced them.

Closing this on the reference scan would read as having answered provenance, which is the thing the issue is about.

What is left, stated as a limit rather than as work pending:

  • The commit that built each image is not recoverable by any mechanism that exists here. No OCI labels, so no org.opencontainers.image.revision. No Actions run, so no head_sha. Both binaries report jmap-mcp/0.0.1, because Cargo.toml carried 0.0.1 until 0ebc967, so the embedded version cannot discriminate between any commit before v0.2.10.
  • The only untried route is rebuilding a candidate commit and comparing layer digests. Slow, decisive, and it either names the commit or proves no commit in the repository produces the image, which is itself an answer.
  • A gate inside the pipeline cannot see an artefact that arrived outside it. The tag-ancestry gate shipped in v0.2.15 closes tags cut off unmerged branches and cannot reach this, because it runs in CI and this path never entered CI.

Branch housekeeping, for the record. fix/rfc8707-absolute-resource and fix/stalwart-jmap-audience are deleted locally. Verified immediately before that git tag --contains b5b1f41 lists v0.2.7 v0.2.8 and cf08af4 lists v0.2.8, and immediately after that both are still commit objects held by those annotated tags. feat/claude-redirects (125f72e) is kept: it is in no tag and on no remote branch, so its local ref is the only thing between that commit and being unreferenced everywhere, and this issue naming the sha is its only other record.

## Why this stays open after the reference scan The scan above closes the **usage** half and settles nothing about the other one. **Nothing breaks if the two images go, but an artefact with no resolvable source is a different question from an artefact nobody uses, and only the second is what was measured.** Two zeroes and a positive control establish that no manifest, no GitOps path, no pod and no public mirror resolves either digest. They say nothing about which commit produced them. Closing this on the reference scan would read as having answered provenance, which is the thing the issue is about. **What is left, stated as a limit rather than as work pending:** - The commit that built each image is not recoverable by any mechanism that exists here. No OCI labels, so no `org.opencontainers.image.revision`. No Actions run, so no `head_sha`. Both binaries report `jmap-mcp/0.0.1`, because `Cargo.toml` carried `0.0.1` until `0ebc967`, so the embedded version cannot discriminate between any commit before v0.2.10. - The only untried route is rebuilding a candidate commit and comparing layer digests. Slow, decisive, and it either names the commit or proves no commit in the repository produces the image, which is itself an answer. - **A gate inside the pipeline cannot see an artefact that arrived outside it.** The tag-ancestry gate shipped in v0.2.15 closes tags cut off unmerged branches and cannot reach this, because it runs in CI and this path never entered CI. **Branch housekeeping, for the record.** `fix/rfc8707-absolute-resource` and `fix/stalwart-jmap-audience` are deleted locally. Verified immediately before that `git tag --contains b5b1f41` lists `v0.2.7 v0.2.8` and `cf08af4` lists `v0.2.8`, and immediately after that both are still `commit` objects held by those annotated tags. `feat/claude-redirects` (`125f72e`) is **kept**: it is in no tag and on no remote branch, so its local ref is the only thing between that commit and being unreferenced everywhere, and this issue naming the sha is its only other record.
Author
Owner

Provenance recovered, 2026-09-02

Both unattributable images are now attributed, by rebuild-and-compare.

v0.2.5   commit 09b5f5b
v0.2.6   tree   29637c1  (commits 8555a9c and c0a9353, identical trees)

Each matched on two independent comparisons: the extracted binary's sha256 and the image's whole config digest.

v0.2.5   published binary  4dcdcc142585413847cf127cf849927398303547e8e576007d7afda7fec7a53f
         rebuilt  binary   4dcdcc142585413847cf127cf849927398303547e8e576007d7afda7fec7a53f
         published config  sha256:99f457738815742ba93d056629995c7b44fe5ad422f2b964383b00539430f87f
         rebuilt  config   sha256:99f457738815742ba93d056629995c7b44fe5ad422f2b964383b00539430f87f

v0.2.6   published binary  8a460ac8b4d2f4f8df564695a057a5225a196736acc94639bba1637617e1bad8
         rebuilt  binary   8a460ac8b4d2f4f8df564695a057a5225a196736acc94639bba1637617e1bad8
         published config  sha256:f22fb733d10a4836eabda0538a1f61ac1bde97db36d58726a2f842b397ad306f
         rebuilt  config   sha256:f22fb733d10a4836eabda0538a1f61ac1bde97db36d58726a2f842b397ad306f

The calibration is why these numbers mean anything

Run first, against v0.2.14, a pair whose answer was already known, before any candidate was built:

v0.2.14  published binary  641460a2384f91567573bb240f2d35ee822fbf33879d40ed1c67a0311f9c7369
         rebuilt  binary   641460a2384f91567573bb240f2d35ee822fbf33879d40ed1c67a0311f9c7369

Without that, a candidate matching nothing would have been indistinguishable from the build not being reproducible on this hardware. The prediction recorded before running was that it would fail, on the reasoning that this host is arm64 and the images are linux/amd64. That reasoning was wrong: the target is amd64 either way, so the output is determined by the digest-pinned builder image, Cargo.lock, and the fixed /build path, all three of which are in git.

Compare the binary, not the layer

A COPY layer's digest is over a tar carrying mtime, uid, gid and mode per entry, so a byte-identical binary can land in a layer with a different digest for reasons unrelated to the compiler. That is a false negative built into the comparison. The binary hash and the config digest are the two that mean something.

What a rebuild can and cannot identify

It identifies a tree, not a commit. 8555a9c and c0a9353 have the identical tree 29637c1, so v0.2.6 is attributable to that tree and not to one of those two commits. Checked before building rather than discovered after.

What this does not change

These images still never went through CI, and the finding stands: six Actions tasks on 2026-08-16, all cargo, all failure, no docker job at all, and the first docker success after 2026-08-14 was two days later. Both are single manifests where every CI-built release here is an index. A gate inside the pipeline cannot see an artefact that arrived outside it.

So provenance is recovered and the hole that produced it is not closed. Those are separate, and only the first was this comment.

## Provenance recovered, 2026-09-02 **Both unattributable images are now attributed**, by rebuild-and-compare. v0.2.5 commit 09b5f5b v0.2.6 tree 29637c1 (commits 8555a9c and c0a9353, identical trees) Each matched on **two independent comparisons**: the extracted binary's `sha256` and the image's whole config digest. v0.2.5 published binary 4dcdcc142585413847cf127cf849927398303547e8e576007d7afda7fec7a53f rebuilt binary 4dcdcc142585413847cf127cf849927398303547e8e576007d7afda7fec7a53f published config sha256:99f457738815742ba93d056629995c7b44fe5ad422f2b964383b00539430f87f rebuilt config sha256:99f457738815742ba93d056629995c7b44fe5ad422f2b964383b00539430f87f v0.2.6 published binary 8a460ac8b4d2f4f8df564695a057a5225a196736acc94639bba1637617e1bad8 rebuilt binary 8a460ac8b4d2f4f8df564695a057a5225a196736acc94639bba1637617e1bad8 published config sha256:f22fb733d10a4836eabda0538a1f61ac1bde97db36d58726a2f842b397ad306f rebuilt config sha256:f22fb733d10a4836eabda0538a1f61ac1bde97db36d58726a2f842b397ad306f ### The calibration is why these numbers mean anything **Run first, against `v0.2.14`, a pair whose answer was already known**, before any candidate was built: v0.2.14 published binary 641460a2384f91567573bb240f2d35ee822fbf33879d40ed1c67a0311f9c7369 rebuilt binary 641460a2384f91567573bb240f2d35ee822fbf33879d40ed1c67a0311f9c7369 **Without that, a candidate matching nothing would have been indistinguishable from the build not being reproducible on this hardware.** The prediction recorded before running was that it would **fail**, on the reasoning that this host is `arm64` and the images are `linux/amd64`. That reasoning was wrong: the target is `amd64` either way, so the output is determined by the digest-pinned builder image, `Cargo.lock`, and the fixed `/build` path, all three of which are in git. ### Compare the binary, not the layer A `COPY` layer's digest is over a tar carrying mtime, uid, gid and mode per entry, so a byte-identical binary can land in a layer with a different digest for reasons unrelated to the compiler. **That is a false negative built into the comparison.** The binary hash and the config digest are the two that mean something. ### What a rebuild can and cannot identify It identifies a **tree**, not a commit. `8555a9c` and `c0a9353` have the identical tree `29637c1`, so `v0.2.6` is attributable to that tree and **not** to one of those two commits. Checked before building rather than discovered after. ### What this does not change **These images still never went through CI**, and the finding stands: six Actions tasks on 2026-08-16, all `cargo`, all `failure`, no `docker` job at all, and the first `docker` success after 2026-08-14 was two days later. Both are single manifests where every CI-built release here is an index. **A gate inside the pipeline cannot see an artefact that arrived outside it.** So provenance is recovered and the hole that produced it is not closed. Those are separate, and only the first was this comment.
Author
Owner

Correction to the provenance comment above, and both attributions now rest on genuine rebuilds

The comment at issuecomment-16198 reported both matches and one of its two lines of evidence was wrong. Corrected here rather than edited, so the mistake stays legible.

What was wrong

Those first candidate builds were not rebuilds. docker build without --no-cache on a machine that had built these images on 2026-08-16 served them from a 93 GB build cache: the dep stub and the source compile were both CACHED, and the run finished rc=0 after 2.5s where a genuine build takes ~79s. So the comparison was cache-hit-and-compare.

And the config-digest match, offered there as independent corroboration, could only have passed in exactly the case it was meant to rule out. The config carries per-layer wall-clock times, 23 history entries each with its own created, so an honest rebuild changes it by construction. A matching config digest means the layers are the originals. It is a cache detector, not a provenance check.

Re-run with --no-cache, and both still match

v0.2.5   tree 09b5f5b        79.4s, `Compiling jmap-mcp v0.0.1` x2, 3 CACHED steps, none a compile
         published binary    4dcdcc142585413847cf127cf849927398303547e8e576007d7afda7fec7a53f  6627632
         rebuilt  binary     4dcdcc142585413847cf127cf849927398303547e8e576007d7afda7fec7a53f  6627632

v0.2.6   tree 29637c1        75.9s, `Compiling jmap-mcp` x2, 3 CACHED steps, none a compile
         published binary    8a460ac8b4d2f4f8df564695a057a5225a196736acc94639bba1637617e1bad8  6627824
         rebuilt  binary     8a460ac8b4d2f4f8df564695a057a5225a196736acc94639bba1637617e1bad8  6627824

The attributions hold. v0.2.5 is tree 09b5f5b; v0.2.6 is tree 29637c1, shared by 8555a9c and c0a9353 and therefore attributable to neither individually.

The binary sha256 is the only comparison that survives, precisely because it is over content with no timestamp in it. The config digests differ on the honest rebuilds, as they must.

Why the calibration could not have caught this

v0.2.14 was built by CI, so this machine held no cache for it and the calibration genuinely compiled. The property that made the calibration honest is the property the subject lacked. A dry run against a known state validates the method and not its application, and the two differ wherever the known state and the subject differ.

Still open, and unchanged by any of this

These images never entered CI, and the gate added in v0.2.15 cannot see them because it runs inside the pipeline they bypassed. Provenance is recovered; the hole is not closed. This issue stays open on the hole.

The two images are kept as the specimens for it. Deleting them while the finding is open would leave the next reader an assertion with nothing behind it.

## Correction to the provenance comment above, and both attributions now rest on genuine rebuilds The comment at `issuecomment-16198` reported both matches and **one of its two lines of evidence was wrong**. Corrected here rather than edited, so the mistake stays legible. ### What was wrong Those first candidate builds were **not rebuilds**. `docker build` without `--no-cache` on a machine that had built these images on 2026-08-16 served them from a 93 GB build cache: the dep stub and the source compile were both `CACHED`, and the run finished `rc=0 after 2.5s` where a genuine build takes ~79s. So the comparison was cache-hit-and-compare. **And the config-digest match, offered there as independent corroboration, could only have passed in exactly the case it was meant to rule out.** The config carries per-layer wall-clock times, 23 history entries each with its own `created`, so an honest rebuild changes it by construction. A matching config digest means the layers *are* the originals. It is a cache detector, not a provenance check. ### Re-run with `--no-cache`, and both still match v0.2.5 tree 09b5f5b 79.4s, `Compiling jmap-mcp v0.0.1` x2, 3 CACHED steps, none a compile published binary 4dcdcc142585413847cf127cf849927398303547e8e576007d7afda7fec7a53f 6627632 rebuilt binary 4dcdcc142585413847cf127cf849927398303547e8e576007d7afda7fec7a53f 6627632 v0.2.6 tree 29637c1 75.9s, `Compiling jmap-mcp` x2, 3 CACHED steps, none a compile published binary 8a460ac8b4d2f4f8df564695a057a5225a196736acc94639bba1637617e1bad8 6627824 rebuilt binary 8a460ac8b4d2f4f8df564695a057a5225a196736acc94639bba1637617e1bad8 6627824 **The attributions hold.** `v0.2.5` is tree `09b5f5b`; `v0.2.6` is tree `29637c1`, shared by `8555a9c` and `c0a9353` and therefore attributable to neither individually. **The binary `sha256` is the only comparison that survives**, precisely because it is over content with no timestamp in it. The config digests differ on the honest rebuilds, as they must. ### Why the calibration could not have caught this `v0.2.14` was built by CI, so this machine held no cache for it and the calibration genuinely compiled. **The property that made the calibration honest is the property the subject lacked.** A dry run against a known state validates the method and not its application, and the two differ wherever the known state and the subject differ. ### Still open, and unchanged by any of this **These images never entered CI**, and the gate added in v0.2.15 cannot see them because it runs inside the pipeline they bypassed. Provenance is recovered; the hole is not closed. This issue stays open on the hole. The two images are **kept** as the specimens for it. Deleting them while the finding is open would leave the next reader an assertion with nothing behind it.
Sign in to join this conversation.
No labels
waiting-on-julian
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jlxq0/jmap-mcp#14
No description provided.