Skip to content

feat(resources): stamp resources set as a human choice (backend#2220) - #539

Merged
shujaatTracebloc merged 3 commits into
developfrom
feature/2220-cli-stamps-provenance
Aug 20, 2026
Merged

feat(resources): stamp resources set as a human choice (backend#2220)#539
shujaatTracebloc merged 3 commits into
developfrom
feature/2220-cli-stamps-provenance

Conversation

@shujaatTracebloc

@shujaatTraceblocshujaatTracebloc commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Stacked on #538 — base is feature/2220-cli-reads-contract. Merge #538 first, then retarget this to develop before merging.

What

Companion to client#768, and the half that makes the marker mean anything.

#768 taught the installer to write env.RESOURCE_PROVENANCE (installer / user / unknown). Without this side, the marker is actively harmful: an edge the installer marked installer, which the operator then re-sized by hand, would keep saying installer — a deliberate human choice wearing the one label that invites a future ladder to overwrite it. That's strictly worse than having no marker at all, so the two PRs are only correct together.

Write side

BuildEnvSpec now writes RESOURCE_PROVENANCE=user, unconditionally.

Unconditional for the same reason NoGPUEnvValue is: the apply runs helm upgrade --reset-then-reuse-values, which re-applies the release's stored values on top of chart defaults, so an omitted key is silently re-inherited, not cleared. There is no branch on which omitting it would be safe. And tracebloc resources set is by definition the human choice — there's no variant of running it that isn't.

Read side

Training.Provenance, normalised through NormalizeProvenance, which maps anything unrecognised — empty, junk, wrong case, a value this binary predates — to unknown, never to a guess.

Returning "" would have made callers branch on empty and invent their own default, which is a second policy and exactly what this ticket removes. Callers must treat unknown as a human choice: it means we don't know, and guessing installer risks overruling an operator.

Surfacing

resources --verbose gains a set by line. Deliberately not the default view: it answers a support question ("did someone set this, or did we?"), not one an operator needs on every run, and a line of bookkeeping that never changes the numbers shouldn't grow the default output.

unknown renders with its explanation rather than the bare word — "predates provenance tracking, treated as an explicit choice" — because the bare word invites the wrong conclusion. It does not mean something is broken.

Notes

  • The env map already flows into helm.UpgradeParams.Env, so no new plumbing — the key reaches the values file for free.
  • The apply-boundary test starts from an installer-marked edge specifically. The unit test proves the map is right; this proves the map arrives. Worth separating because the failure is silent: a marker that never lands looks identical to one that did, and the consequence surfaces much later.
  • zz-all-strings.golden regenerated — one line, "set by", reviewed rather than rubber-stamped.

Verification

  • go build, go vet clean; go test ./... — all packages green
  • gofmt, goimports clean; deadcode-check.sh exit 0
  • 6 new tests in internal/resources (including a set-then-read round trip, the end-to-end invariant: what resources set writes, the read path reports as a human choice) + 1 apply-boundary test in internal/cli

Also in this branch

#538 needed a follow-up commit: the Lint job's deadcode gate flagged ContractVersion as an unreachable export — I'd added it "for doctor output" and never wired it up, which is exactly the shape that gate exists to catch. Removed; the in-package tests use the field directly. The pre-existing stale allowlist entry for doctor.go: Status.String is untouched — it's reported on clean develop too and is a warning, not a failure.

Refs: RFC-BACKEND-664 §P0, client#768, client-runtime#358, cli#538

🤖 Generated with Claude Code


Note

Cursor Bugbot is generating a summary for commit c947c29. Configure here.

@shujaatTraceblocshujaatTracebloc self-assigned this Aug 20, 2026
Base automatically changed from feature/2220-cli-reads-contract to developAugust 20, 2026 10:57
@shujaatTracebloc
shujaatTracebloc marked this pull request as ready for review August 20, 2026 10:58
Comment threadinternal/resources/set.go

@saadqbalsaadqbal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The design is right and the reasoning for unknown-never-a-guess is the part I'd have wanted argued: NormalizeProvenance mapping empty, junk, wrong case and future-unknown values all to unknown rather than returning "" and making every caller invent its own default is the correct shape.

Bugbot's High is real, and I confirmed it by reading — and the fix is already sitting three lines below the bug.

internal/cli/resources_set.go:240-244:

ceilingUnchanged:=sameCeiling(desired, current)
ifceilingUnchanged&&!phantomGPU {
p.Successf("Each training run already uses up to %s — nothing to change.", …)
returnnil// BuildEnvSpec is never reached
}
ifceilingUnchanged { // phantomGPU == true here// Don't treat it as a clean no-op — fall through to persist so BuildEnvSpec's// explicit-empty GPU override lands …
}

So BuildEnvSpec stamps RESOURCE_PROVENANCE=user unconditionally within itself, but the caller can exit before reaching it. An installer-sized edge where the operator runs resources set --max, or passes flags restating the current ceiling, exits "nothing to change" and keeps installer.

That's precisely the state BuildEnvSpec's own comment calls "the single most dangerous state the marker can be in — a human choice wearing a label that invites overwriting". And the phantom-GPU branch immediately below is the template: same condition, same reason, already written. Extending it to also fall through when the stored provenance isn't already user is a two-line change with the rationale pre-argued.

This matters for the pair's stated correctness condition. Your body says #768 and this "are only correct together", because the marker is actively harmful without the user side. With this hole open, they aren't correct together either — just narrower: the same-size set still leaves a deliberate choice labelled overwritable. Worth fixing before either lands rather than after both.

Two smaller notes:

  • The stacked-PR banner is stale. It says base is feature/2220-cli-reads-contract and to retarget after #538 — but #538 merged at 10:57 and this PR's base already reads develop. Worth dropping the note so nobody retargets something already retargeted. (Also, for the record, the org standard is no stacked PRs on open ones — resolved here by #538 landing first, which is the right outcome.)
  • BuildEnvSpec writing unconditionally is right, and the --reset-then-reuse-values reasoning is the same one NoGPUEnvValue rests on: an omitted key is silently re-inherited, not cleared. Good that it's stated rather than assumed.

Requesting changes on the sameCeiling path; the Bugbot thread blocks the merge regardless. Everything else here I'd own.

shujaat hasanand others added 2 commits August 20, 2026 13:40
Companion to client#768, and the half that makes the marker mean anything.
client#768 taught the installer to write env.RESOURCE_PROVENANCE
(installer | user | unknown). Without this side, an edge the installer marked
`installer` and the operator then re-sized by hand would KEEP saying
`installer` -- a deliberate human choice wearing the one label that invites a
future ladder to overwrite it. That is strictly worse than no marker at all,
so the two PRs are only correct together.
BuildEnvSpec now writes RESOURCE_PROVENANCE=user unconditionally. Unconditional
for the same reason NoGPUEnvValue is: the apply runs
`helm upgrade --reset-then-reuse-values`, which re-applies the release's stored
values on top of chart defaults, so an omitted key is silently RE-INHERITED
rather than cleared. There is no branch on which omitting it would be safe.
`tracebloc resources set` is by definition the human choice -- there is no
variant of running it that isn't.
Read side: Training.Provenance, normalised through NormalizeProvenance, which
maps anything unrecognised -- empty, junk, a value this binary predates, wrong
case -- to `unknown`, never to a guess. Returning "" would have made callers
branch on empty and invent their own default, which is a second policy and
exactly what this ticket removes. Callers MUST treat `unknown` as a human
choice: it means we do not know, and guessing `installer` risks overruling an
operator.
Surfaced in `resources --verbose` as "set by", not in the default view: it
answers a support question ("did someone set this, or did we?"), not one an
operator needs on every run, and a line of bookkeeping that never changes the
numbers should not grow the default output. `unknown` renders with its
explanation rather than the bare word, which invites the wrong conclusion -- it
does not mean something is broken.
The env map already flows into helm.UpgradeParams.Env, so no new plumbing. The
added apply-boundary test starts from an installer-marked edge specifically,
because the unit test proves the map is right while this proves the map arrives
-- and a marker that never lands looks identical to one that did, with the
consequence surfacing much later.
zz-all-strings.golden regenerated: one line, "set by", reviewed.
Verified: go build + go vet clean, go test ./... all packages green, gofmt and
goimports clean, deadcode exit 0. 6 new tests in internal/resources (including a
set-then-read round trip) + 1 apply-boundary test in internal/cli.
Refs: RFC-BACKEND-664 P0, client#768, client-runtime#358, cli#538
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… review, #539)
Bugbot's High, confirmed by @saadqbal in review, and the fix was indeed sitting
three lines below the bug.
BuildEnvSpec stamps RESOURCE_PROVENANCE=user unconditionally WITHIN ITSELF, but
the caller could exit before ever reaching it: `sameCeiling` returned "nothing to
change" whenever the desired ceiling matched the current one. So an
installer-sized edge whose operator ran `resources set --max`, or passed flags
restating the current ceiling, kept `installer` -- a deliberate human choice
wearing the one label that invites a future ladder to overwrite it. That is
verbatim the state BuildEnvSpec's own comment calls the most dangerous the marker
can be in, and my PR body claimed this PR and client#768 "are only correct
together" while this hole made that untrue: not wrong, just narrower.
Fixed with the phantom-GPU branch as the template -- same condition, same
reason, already written and already reviewed. An unchanged ceiling now falls
through to persist when the stored marker is not already `user`. Both conditions
can hold at once, so both report their reason rather than one masking the other.
`unknown` counts as stale deliberately. A pre-marker edge whose operator restates
the ceiling has just made that size explicit, so recording it is the honest
answer; the cost is one extra apply per edge, exactly once, because the second
run sees `user` and is a clean no-op again.
Three existing no-op tests now carry RESOURCE_PROVENANCE=user in their fixtures
(TestSet_NoOpSkipsApply, TestSet_NoOpEvenWhenCurrentNoLongerFits,
TestWizard_LeaveAsIs). Their invariant is unchanged and still asserted -- a clean
no-op makes no helm call -- it just needs the marker present to BE the clean
case. The uncovered case became TestSet_SameCeilingStampsProvenance, a table over
installer / pre-marker / junk, asserting the apply happens, that we do not claim
nothing changed while correcting the marker, and that the reason is stated.
zz-all-strings.golden regenerated: one line, reviewed.
Also rebased onto develop now that cli#538 has merged -- `git rebase --onto
origin/develop 05517ed` to drop the squashed parent's commits rather than
replay them into conflicts. GitHub had already retargeted the base to develop
when #538 landed, so the stacked-PR banner in the description was stale; removed.
Verified: go build + go vet clean, go test ./... all packages green, gofmt and
goimports clean, deadcode exit 0.
Refs: backend#2220, client#768
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@shujaatTracebloc
shujaatTraceblocforce-pushed the feature/2220-cli-stamps-provenance branch from c947c29 to 16aa63bCompareAugust 20, 2026 11:42

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 16aa63b. Configure here.

Comment threadinternal/cli/resources_set.go

@saadqbalsaadqbal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

My finding is fixed, and fixed the way I'd hoped — conditionally, not unconditionally:

staleProvenance:=current.Provenance!=resources.ProvenanceUserifceilingUnchanged&&!phantomGPU&&!staleProvenance { … returnnil }

So the clean no-op survives for an edge already marked user, and treating unknown as stale is the honest call with the cost stated ("one extra apply per edge, exactly once"). Good.

But Bugbot's new Medium is real, and I verified both halves across the two repos — it's worse than "one extra apply once".

The claim is that current.Provenance can never become user, so staleProvenance is stuck true. I checked the two things it depends on:

  1. The CLI pins the chart version.internal/helm/upgrade.go:68"pin --version to the currently-installed chart version" — with the reasoning at resources_set.go:555 that omitting it would pull the latest chart and silently change the whole client. Correct behaviour, and load-bearing here.
  2. Only chart 1.9.50 emits the key onto the pod.client#768 adds it at jobs-manager-deployment.yaml:477 (hasKey .Values.env "RESOURCE_PROVENANCE", else "unknown"), in the bump to 1.9.50.

Put those together and it doesn't converge. On any edge running ≤ 1.9.49:

  • ParseTraining reads the pod env, which has no RESOURCE_PROVENANCE, so current.Provenance is unknown;
  • staleProvenance is true, so the no-op never returns and every resources set runs a helm upgrade;
  • that upgrade is pinned to the old chart, which still doesn't emit the key, so the next read is unknown again.

It's permanently non-idempotent on every existing edge, not once. And the tail Bugbot points at is the sharp end: off a TTY without --yes, that path now reaches the confirmation gate and exits 1 where it previously printed "nothing to change" and exited 0. Anything scripted around a restating resources set breaks.

The fix I'd reach for is to read the marker where it's written.resources set writes RESOURCE_PROVENANCE into Helm values; the pod env is a downstream projection gated on chart version. Reading it back from helm get values makes the round-trip self-consistent and independent of which chart is installed — and it's what the installer side already does (Get-CarriedTrainingValues reads values, not pod env).

Failing that, the two narrower options are to treat "installed chart predates the key" as its own state and no-op in it, or to gate staleProvenance on the installed chart version. Both work; both need the version comparison the first option avoids entirely.

Also: version-bump-gate / version-check is failing. Separate from the above and I haven't dug into it — worth a look since it's a required-looking gate rather than a flake.

Keeping changes-requested. My original ask is genuinely closed, so this is a new finding rather than a repeat; and per my note on client#768, that one is approved but shouldn't ship ahead of this.

v0.10.9 is already released and this PR changes published files under
internal/*, so the release train needs a version above every released tag —
version-bump-gate fails otherwise (backend#1561).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@shujaatTracebloc

Copy link
Copy Markdown
ContributorAuthor

@saadqbal ready for re-review.

Both points from the last pass are closed — one with a fix, one with a measurement.

version-bump-gate / version-check — fixed (5a1539f). Not a flake, a real stale-version gate: VERSION still said 0.10.9, v0.10.9 is already released, and this PR changes published files under internal/*. Bumped to 0.10.10, derived from the live release list rather than the log (highest released final is still 0.10.9, and no other open cli PR touches VERSION, so there's no collision). CI is now fully green.

The new Medium — I believe it's a false positive, and I checked it by rendering the chart rather than reading it. Full evidence is on the Bugbot thread; the short version:

Premise 2 ("only chart 1.9.50 emits the key onto the pod") doesn't hold. jobs-manager-deployment.yaml has the named hasKey blocks and a generic {{- range $key, $value := .Values.env }} passthrough whose filter is a denylist:

{{- if and (ne $key "CLIENT_ENV") (ne $key "RESOURCE_REQUESTS") (ne $key "RESOURCE_LIMITS") (ne $key "GPU_REQUESTS") ... $value }}

On charts ≤ 1.9.49RESOURCE_PROVENANCE is not in that list, so the generic loop emits it. helm template with env.RESOURCE_PROVENANCE=user:

1.9.49 (develop) container[0] api: RESOURCE_PROVENANCE x1 = ['user']
1.9.44 (released) container[0] api: RESOURCE_PROVENANCE x1 = ['user']
1.9.50 (#768) container[0] api: RESOURCE_PROVENANCE x1 = ['user']

container[0] is exactly what the read path consumes (JobsManagerEnv takes Containers[0]). I checked every released tag carrying the unified client/ chart — all have the generic passthrough, so there is no installed version that drops the key. values.schema.json also sets env.additionalProperties: {"type":"string"}, so an undeclared string key is accepted.

The neat inversion: #768 addsRESOURCE_PROVENANCE to the denylist (lines 544/636) to avoid double-emitting alongside its new named block — and that addition is precisely why 1.9.50 is the only version where the named block is the emission path. Rendered 1.9.50: exactly one entry, no duplicate.

So it converges on every chart: read is user on the second run, the clean no-op returns, and the off-TTY --yes-less path keeps exiting 0. Cost stays the documented one — a pre-marker edge pays one extra apply, once.

Your premise 1 (the CLI pins --version) is correct and I'm not disputing it; it just isn't load-bearing once the key travels by the generic path. I'd rather not switch the read to helm get values on a premise that measurement contradicts — happy to if you still see a hole, but it'd be trading a working path for a second source of truth.

Not changed: the stale stacked-PR banner. You're right that it should go (base already reads develop), but editing the PR description was blocked by a permission gate in my session — flagging it rather than claiming it done.

@saadqbalsaadqbal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@shujaatTracebloc is right and I was wrong. Approving.

I verified the rebuttal against develop's chart rather than accepting it, and it holds. Chart 1.9.49 carries the generic passthrough at jobs-manager-deployment.yaml:514 and :611, and its denylist at :532/:612 is:

(ne $key "CLIENT_ENV") (ne $key "RESOURCE_REQUESTS") (ne $key "RESOURCE_LIMITS")
(ne $key "GPU_REQUESTS") (ne $key "GPU_LIMITS") (ne $key "RUNTIME_CLASS_NAME")
(ne $key "SINGLE_NODE") (ne $key "CLIENT_ID") (ne $key "IMAGE_PULL_SECRET_NAME")

No RESOURCE_PROVENANCE. So on the old chart the key rides the generic passthrough onto the pod, ParseTraining reads it, staleProvenance goes false after one apply, and the no-op returns. It converges.

Where I went wrong is worth naming precisely, because it wasn't a guess — it was a verification that stopped one step short. I checked #768's template, saw RESOURCE_PROVENANCE in its denylist, saw the named emit only in 1.9.50, and concluded older charts can't deliver the key. But #768 adds it to the denylist because it adds the named emit — otherwise the key would be emitted twice. The old chart doesn't exclude it, which is exactly why it gets through. Two charts, each self-consistent, both delivering the key by different routes. I confirmed the half that supported the finding and never tested the half that would have refuted it.

Rendering the chart rather than reading it is the right instinct and it's what settled this.

On the PR itself — my original sameCeiling finding is fixed the way I hoped, conditionally rather than unconditionally:

staleProvenance:=current.Provenance!=resources.ProvenanceUserifceilingUnchanged&&!phantomGPU&&!staleProvenance { … returnnil }

so the clean no-op survives for an edge already marked user, and treating unknown as stale is the honest call with the one-extra-apply cost stated rather than hidden. 5a1539f6 clears the version gate. 24 checks passing, nothing pending, no open threads.

The design I said I'd own still stands: NormalizeProvenance mapping empty, junk, wrong case and future-unknown all to unknown rather than returning "" and making each caller invent a default, and BuildEnvSpec writing the marker unconditionally for the --reset-then-reuse-values reason NoGPUEnvValue already rests on.

Merge ordering still applies, and it's now the only thing I'd flag: this and client#768 are only correct together, and I've approved both. Ship them together rather than either alone.

@shujaatTracebloc
shujaatTracebloc merged commit 96d57d3 into developAug 20, 2026
27 checks passed
@shujaatTracebloc
shujaatTracebloc deleted the feature/2220-cli-stamps-provenance branch August 20, 2026 12:13
saadqbal added a commit that referenced this pull request Aug 21, 2026
… (cli#546) (#547)
Bugbot MEDIUM on the develop->staging promotion mirror (#540), against #539
(backend#2220).
#539 made any non-`user` RESOURCE_PROVENANCE stale, so an unchanged ceiling
stopped returning early and instead fell through to the apply in order to
re-stamp the marker as `user`. Correct intent, wrong landing site: the
fall-through lands in the confirmation gate, and off a terminal that gate does
not ask -- it returns exit 1. So
tracebloc resources set --cores 4 --memory 16 # the CURRENT ceiling
went from the exit-0 no-op the command's own --help documents ("0 applied (or
nothing to change)"), and that docs/cli-navigation.md draws as an edge going
straight to exit 0 bypassing CONF, to a hard failure.
Nearly every installed edge still reads `installer` or `unknown`, so the blast
radius was the installed base rather than an edge case, and the callers that
restate a size are scripts -- the bootstrap, the end-to-end journey -- none of
which pass --yes for what the docs promise is a no-op.
The fix is one clause on the gate, not a revert: the gate guards the CEILING,
and an unchanged ceiling has nothing to ask about ("Let each training run use
up to 4 CPU / 16 GiB?" when the answer is already that is a question with one
honest answer). The re-stamp #539 added still happens. The phantom-GPU
fall-through (#241) had the same shape and is fixed by the same clause: both
are bookkeeping writes, already announced by their own Infof lines, not budget
changes an operator needs to sanction.
Tests: TestSet_SameCeilingNeedsNoYes. Every same-ceiling case already in the
file passed `yes: true` -- the flag under dispute -- which is why nothing was
red. The new cases assert exit 0 AND that the apply still happens, because
either alone is satisfiable by the wrong fix; they read RESOURCE_PROVENANCE=user
off the values file helm was actually handed (the existing assertions go through
--dry-run, which skips the very gate at issue); they cover the phantom-GPU
sibling and a declining prompter on a terminal; and one sub-case bounds the fix
by proving a real CHANGE off a terminal still exits 1 and mutates nothing.
Five of the six sub-cases fail without the one-clause change.
docs/cli-navigation.md: the `no change` node now says it may still re-stamp
provenance or clear a phantom GPU, and never asks to confirm -- true of #539's
behaviour too, which shipped without updating the map.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor

/fr-pass

Sign up for freeto 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

@shujaatTracebloc@LukasWodka@saadqbal