Skip to content

feat: add the ModelArtifact expectedDigest anchor - #702

Merged
thxCode merged 10 commits into
mainfrom
feat/model-artifact-expected-digest
Sep 29, 2026
Merged

thxCode merged 10 commits into
mainfrom
feat/model-artifact-expected-digest

Conversation

@thxCode

@thxCode thxCode commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

What type of PR is this?

/kind enhancement
/kind api-change

/area worker
/area testing

What this PR does / why we need it:

Adds an optional expectedDigest to the ModelArtifact spec: the manifest digest the user asserts this artifact resolves to. The manifest format v1 was published precisely so such an assertion could later be compared with it; this PR opens that comparison and what it unlocks.

  • Admission. expectedDigest validates as sha256: + 64 lowercase hex and is accepted on hub sources only — a claim's content is whatever the volume holds at mount time (dynamically provisioned claims differ per provisioning) and its identity is the claim itself, which the user confirms; an image's identity is its reference's digest. Immutable with the whole spec.
  • Resolution asserts. A resolution whose digest differs from the anchor is refused as DigestMismatch with both digests in the message, and DigestMismatch joins the revoking reasons. The periodic revalidation of an anchored artifact re-lists the tree at the resolved commit and re-canonicalizes instead of probing one file, so a hub or mirror that stops serving the pinned content fails the same staircase; unanchored artifacts keep the two-request check.
  • A confirmed outage anchors the identity. SourceUnavailable confirmed on the refusal staircase (one blip is not a verdict) writes the anchor as the resolution: status.resolved.manifestDigest = the anchor, digestSource: Expected, no revision or counts — and the hub is never contacted again: no revalidation, no token probe. A hub that answered is never anchored around.
  • Delivery. An anchored artifact is refused under Engine delivery (AnchorNeedsNodeDelivery: the engine downloads by repository and revision and cannot anchor-verify what it fetched; an Expected identity has no commit to pin). An Expected identity's node chain is peers only: the manifest comes from a peer's published listing bound to the digest (Puller.FetchManifest), and with no peer holding the tree the mount fails loudly naming the digest nothing holds.
  • Documented access model. Integrity is never at risk — only anchor-named, verified bytes enter a published set or a mount. For Expected identities no credential participates: digest knowledge becomes the capability, and digests are visible cluster-wide on NodeModelStore status (adjudicated and documented in docs/model-store/artifact.md); hub-verified identities keep resolving with the namespace's own credential. enhancement: migrate pre-existing GPUStack model files into the node cache #693 (legacy-cache migration) is deferred: expectedDigest is recorded in the docs as the acceptance contract that future import must satisfy — verify against the anchor, publish only through the store's pipeline, lazy, one-shot.

E2E evidence: case-1 and the new case-114 (eight checks: match resolves digestSource: Hub; a foreign anchor refused DigestMismatch naming both digests; a twice-unreachable hub writing the Expected identity with the hub's request log flat 9 → 9; AnchorNeedsNodeDelivery on both anchored kinds with no Pod created; the Expected identity mounting with the hub log still flat) ran on kind v1.35.5 against image s11-e2e, built from this branch's head 42c34e1f. The current head 355129ad adds only the spec document and the #701 rebase, so the tested tree is equivalent for every code path. test chart (chart-testing install of the full stack) ran green on the same head family. Per-task commits carry the unit suites; AC2/AC3 are mutation-verified red (21fcec69 records the KV pin and the nil-hub fix the end-of-build review caught).

Which issue(s) this PR links to:

NONE

Special notes for your reviewer:

  • Version floors are unchanged: functional Kubernetes 1.29, chart install 1.23. The API additions are plain CRD fields — no CEL, no new admission mechanism.
  • The stale ModelScope member comment, left reserved-shaped by feat(worker): open the ModelScope ModelArtifact source end to end #699, is corrected here (pure comment).
  • case-114 flips two Settings (hub endpoint, delivery mode) and restores both in its trap; its peer leg auto-skips on one worker or with E2E_C114_OFFLINE=1.
  • The Verified condition stays deferred: its only novel scenario was a claim anchor, which this PR deliberately does not open.

Does this PR introduce a user-facing change?

New `ModelArtifact` field `spec.expectedDigest`: assert the manifest digest a Hugging Face or ModelScope artifact must resolve to. A resolution that serves different content is refused with both digests in the message, the periodic check re-verifies the assertion instead of probing one file, and an artifact whose hub cannot be reached resolves to the anchor without the hub (`status.resolved.digestSource: Expected`) — it then delivers only from the node cache's peers and is never mounted through an engine download. Refused on claim and image sources, immutable with the spec.

- spec.expectedDigest asserts the manifest digest a hub source must resolve to;
  admission accepts it on hub sources only, refusing claim and image sources each
  with the reason their identity rules it out, and the whole-spec immutability
  ratchet covers it
- status.resolved.digestSource records where a resolved digest came from: Hub, the
  hub's own listing, or Expected, the spec's anchor
- the stale modelscope member comment, left reserved-shaped by #699, now describes
  the opened source
- generated deepcopy, crd, protobuf, openapi and applyconfiguration artifacts

Task 1 of model-artifact-expected-digest.

Signed-off-by: thxCode <thxcode0824@gmail.com>
- a resolution whose manifest digest differs from the anchor is refused as
  DigestMismatch with both digests in the message, and DigestMismatch joins the
  revoking reasons
- the anchored revalidation re-lists the tree at the resolved commit and
  re-compares the digest instead of probing one file; the unanchored probe is
  unchanged
- a SourceUnavailable confirmed on the refusal staircase writes the anchor as
  the identity (digestSource Expected, no revision or counts) and the artifact
  never contacts the hub again
- the hub interface grows the manifest-returning ListManifest both clients
  already serve
- mutations prove the comparison and the anchored revalidation tests go red

Task 2 of model-artifact-expected-digest.

Signed-off-by: thxCode <thxcode0824@gmail.com>
…ly chain

- an anchored artifact under Engine delivery waits with AnchorNeedsNodeDelivery,
  the message naming why: an engine downloads by repository and revision and
  cannot anchor-verify what it fetched, and an Expected identity has no commit
- an Expected identity's node chain is peers only: the manifest comes from a
  peer's published listing bound to the digest (Puller.FetchManifest), and with
  no peer holding the tree the mount fails naming the digest nothing holds
- the hub-identity chain is unchanged, still listing anchor-checked before any
  byte moves

Task 3 of model-artifact-expected-digest.

Signed-off-by: thxCode <thxcode0824@gmail.com>
- artifact.md: the expectedDigest field and its hub-only admission, the
  anchored resolution and revalidation states, digestSource, the confirmed
  SourceUnavailable fallback and what an Expected identity delivers, the
  shipped access model for Expected identities, the AnchorNeedsNodeDelivery
  row, the migration contract, and the downgrade-window note
- node-store.md: the Expected identity's peers-only manifest and the
  SourceUnavailable row's anchored case
- README index: the artifact page's description carries the anchor

Task 4 of model-artifact-expected-digest.

Signed-off-by: thxCode <thxcode0824@gmail.com>
- a match resolves with digestSource Hub; a foreign anchor is refused
  DigestMismatch with both digests in the message
- a twice-unreachable hub writes the anchor as the identity with the hub's
  request log flat, and the Expected identity mounts from the peer with the
  hub log still flat
- Engine delivery holds both anchored artifacts with AnchorNeedsNodeDelivery
  and no Pod
- the peer leg skips on one worker or with E2E_C114_OFFLINE=1; the run restores
  both Settings it flips

Task 5 of model-artifact-expected-digest.

Signed-off-by: thxCode <thxcode0824@gmail.com>
…V pin

- fetchTree built every file's URL by calling the hub, which is nil on the
  peers-only path and would panic before a peer byte was pulled; the URL is
  now empty when there is no hub, and the per-file loop's no-hub branch was
  already the loud no-source failure
- the KV identity pin the spec's acceptance names: the digest's leading digits
  for a hub artifact and for an Expected identity alike, the UID's hash only
  for a claim

Task 5 of model-artifact-expected-digest.

Signed-off-by: thxCode <thxcode0824@gmail.com>
The artifact helper appends its last argument under spec:, six spaces landed
the field inside the huggingFace member and strict decoding refused every
anchored artifact the case created.

Task 5 of model-artifact-expected-digest.

Signed-off-by: thxCode <thxcode0824@gmail.com>
The replica render synthesizes the runner image from the instance type's
observed hardware; the cluster's generic type has none, so the render failed
before WeightsReady could carry the anchor block. Name the stock python image
on the role — the block creates no Pod whatever the image is.

Task 5 of model-artifact-expected-digest.

Signed-off-by: thxCode <thxcode0824@gmail.com>
T1-T6 delivered: the anchor's admission, assertion at resolution and
revalidation, the confirmed-outage Expected identity, the peers-only chain,
the Engine block, the documentation, and the e2e case. The plan-gate
adjudications are recorded in the spec's Open Questions and Alternatives.

Task 6 of model-artifact-expected-digest.

Signed-off-by: thxCode <thxcode0824@gmail.com>
@thxCode thxCode added kind/enhancement New feature or request kind/api-change Adds, removes, or changes an API area/worker The worker control plane and its controllers area/testing End-to-end test infrastructure labels Sep 29, 2026
@gpustack-code-review

gpustack-code-review Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

✅ OpenCodeReview: Review complete: 0 finding(s) across 1 selected item(s).

Reviewed 355129a..f91eb78 only; earlier commits in this PR were reviewed in a previous run.

Comment thread pkg/modelmanager/materialize/materialize.go
…nd class

fromPeersOnly replaced FetchManifest's error wholesale: a cancellation under
the listing — the waiter window firing — was misclassified as
SourceUnavailable and consumed the digest's backoff, and every other
underlying cause never reached the plugin's log. A cancellation now keeps its
canceled class (no backoff, the shape fetchTree's wait already uses), and any
other failure logs its cause beside the loud no-source. The ledger's
tenant-free message is unchanged.

Task 5 of model-artifact-expected-digest.

Signed-off-by: thxCode <thxcode0824@gmail.com>
@thxCode
thxCode merged commit a4c5c0b into main Sep 29, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/testing End-to-end test infrastructure area/worker The worker control plane and its controllers kind/api-change Adds, removes, or changes an API kind/enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant