Skip to content

feat(api): speak the API-controlled CLI version protocol [VOL-180]#39

Open
tkkhq wants to merge 6 commits into
mainfrom
feat/api-version-protocol
Open

feat(api): speak the API-controlled CLI version protocol [VOL-180]#39
tkkhq wants to merge 6 commits into
mainfrom
feat/api-version-protocol

Conversation

@tkkhq

@tkkhq tkkhq commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces the GitHub-release-based update-notice check (VOL-168) with the Volcano Hosting API as the source of truth for CLI version signaling, per VOL-180. The CLI now reports its own version on every API request and acts on whatever instruction the API returns, instead of independently polling GitHub on every command invocation.

Server counterpart: volcano-hosting#522 (gate + telemetry, merge-ready) and volcano-hosting#532 (dormant policy-arming, do-not-merge-yet draft). This PR is code-complete and does not depend on those being merged first — the protocol is additive and the server ships the gate dormant either way.

Changes

  • internal/api: a versionProtocolDoer wraps every HTTP call the generated client makes (including unauthenticated ones like device/token exchange) to:
    • Set X-Volcano-CLI-Version and a real User-Agent: volcano-cli/<version> (<os>/<arch>), replacing Go's default Go-http-client UA.
    • Record X-Volcano-CLI-Instruction / X-Volcano-CLI-Latest-Version / X-Volcano-Device-Instruction response headers via the new api.LastInstructions(). One CLI process runs exactly one command, so this is safe as mutex-guarded process-global state.
  • internal/cmd/upgrade: removed the GitHub-polling notice machinery (MaybePrintUpdateNotice + its on-disk cache) entirely. Added PrintAPIInstructionNotices, which renders a non-blocking upgrade suggestion or a deprecation warning straight from api.LastInstructions() — zero extra network round-trips, since it only reads headers the command's own API calls already received (previously: up to a 2s GitHub call before every command even started, unless cached).
    • volcano upgrade's actual download/checksum/cosign-verify flow (internal/update) is unchanged — GitHub remains the download/release-artifact source; only the "should I nudge/require an upgrade" decision moves to the API.
  • cmd/volcano/main.go: prints notices unconditionally after Execute() returns (cobra skips PersistentPostRun when RunE errors, so this can't live there or a 426/exempt-route case would be missed). A 426 require_version_upgrade error gets the concrete upgrade target appended when the API provided one; any other error gets a run volcano login hint when the API signaled device_instruction=reauth.

Security model

The claimed version has no cryptographic binding to a real release — volcano-cli is open source, so per-version keypairs/checksums for client attestation were considered and rejected as unwinnable (see docs/cli/version-gating.md in volcano-hosting). This is intentional: the header is advisory input the CLI cooperates with, never a privilege — spoofing it can only ever escape a restriction (run an old/fake version, get told to upgrade less), never escalate one. All authority stays in the bearer credential.

Reserved for future work

CLIInstructionLowCreditWarning / CLIInstructionNotEnoughCredit (low_credit_warning / not_enough_credit) are wired into PrintAPIInstructionNotices — real, tested cases, not just reserved constants — per review discussion on volcano-hosting#522. Unreached today: the server never emits them (see volcano-hosting's matching creditgate no-op scaffold), so once it does, this side needs zero changes to start working.

The printed notice is a placeholder, not the designed UX. marckong's actual ask is an interactive prompt ("upgrade?" / "purchase extra credits?") that takes a real action — needs a target (URL? command?) that doesn't exist yet. internal/confirm already has the prompt primitive for that once there's something concrete to confirm. Billing lives in a separate service, so the real gate + this CLI's interactive response to it needs its own design pass once that integration is scoped.

Test

  • internal/api: versionProtocolDoer sets both headers, is a no-op on transport error, and merges per-field rather than overwriting — an earlier real instruction survives a later response that simply doesn't repeat the header (a genuine bug caught in review); full round-trip through NewClient + a real generated-client call.
  • internal/cmd/upgrade: suggestion, deprecation, and the reserved credit-instruction cases, white-label CommandPathPrefix rendering — all driven through a real api.Client call against a test server.
  • cmd/volcano: run() itself is tested (not just the extracted print helpers) — 426 short-circuit without duplicating the notice, error-before-notice ordering, reauth ordering, and the deprecation notice on a successful/exempt-route response.
  • Full suite green: go build ./..., go test ./... -count=1, -count=5 -shuffle=on on the touched packages, make lint (0 issues), make tidy (no go.mod/go.sum diff).

Copilot AI review requested due to automatic review settings July 8, 2026 16:18
@tkkhq tkkhq requested a review from a team as a code owner July 8, 2026 16:18

Copilot AI 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.

Pull request overview

This PR moves CLI version signaling (upgrade suggestion, deprecation, reauth hint) from a GitHub-release polling mechanism to an API-driven protocol (VOL-180). The generated API client now reports the CLI version on every request and captures server instructions from response headers; the CLI then renders notices based on the observed instructions without adding extra network calls.

Changes:

  • Added a VOL-180 “version protocol” transport wrapper that sets X-Volcano-CLI-Version + a custom User-Agent, and records instruction headers for later consumption.
  • Replaced GitHub-polling update notices with PrintAPIInstructionNotices, which renders suggestion/deprecation notices based on api.LastInstructions().
  • Updated the CLI entrypoint to print notices after command execution and to enrich 426 “version_deprecation” errors with a concrete target version when provided.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
internal/api/version_protocol.go Implements request header injection and response instruction recording via a transport wrapper + process-global snapshot.
internal/api/version_protocol_test.go Validates header setting, instruction recording, error/no-response behavior, and “latest response wins” semantics.
internal/api/client.go Wraps both standard and streaming HTTP doers with the version protocol wrapper in NewClient.
internal/api/client_test.go Asserts NewClient sends VOL-180 headers and records instruction headers end-to-end.
internal/cmd/upgrade/upgrade.go Removes GitHub-based polling notice code and adds PrintAPIInstructionNotices + new notice renderers.
internal/cmd/upgrade/upgrade_test.go Replaces cache/polling tests with instruction-driven notice tests using a real API client call.
internal/cmd/root/root.go Removes the prior PersistentPreRun update notice hook.
cmd/volcano/main.go Prints API-driven notices post-Execute(), enriches 426 errors with latest-version target, and adds reauth hinting.
cmd/volcano/main_test.go Tests printDeprecationError and printError behaviors driven by recorded instruction headers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

tkkhq added a commit that referenced this pull request Jul 9, 2026
All 6 findings verified against the actual code before fixing.

1. [P3] Fixed a stale comment claiming PrintAPIInstructionNotices has no
   case for the reserved credit instructions \u2014 the credit-hook commit
   added exactly those cases, but the comment written before that landed
   was never updated.

2. [P3] recordInstructions previously overwrote the full Instructions
   snapshot on every response, so a command making multiple API calls
   could have a real notice (suggestion, deprecation, or reauth) silently
   dropped by a later, unrelated response that simply didn't repeat the
   header. Now merges per-field: CLIInstruction and its paired
   LatestVersion update together (they come from the same server-side gate
   decision), DeviceInstruction updates independently, and neither is
   cleared by an empty value \u2014 only ever replaced by a new non-empty one.
   Added ResetLastInstructionsForTest (exported, test-only) since the
   sticky behavior means tests can no longer rely on an unrelated response
   to reset state between cases.

3. [nit] main's non-blocking-error path printed the pending notice before
   the actual "Error:" line. Reordered so the failure always prints first
   \u2014 both for humans skimming stderr and for log parsers/scripts that
   assume line 1 is the error.

4. [nit] Extracted main()'s orchestration (426 short-circuit, error-before-
   notice ordering, exit codes) into a testable run(root, deps) int
   function, and added tests driving it through real commands against a
   test server (success/no-notice, success/suggestion, 426 short-circuit
   without duplicating the notice, non-blocking error ordering, reauth
   ordering) \u2014 previously only the extracted print helpers were tested,
   not this orchestration itself.

5. [nit] Fixed a comment that wrapped api.CLIInstructionNotEnoughCredit
   across two lines mid-identifier, reading like a typo.

6. [nit] Removed the now-unused update.Notice type, the last remnant of
   the removed GitHub-polling notice model.

Verified: go build/vet clean, full test suite green (including -count=5
-shuffle=on on the changed package to rule out the new sticky-state
cross-test contamination this surfaced and fixed), make lint 0 issues,
make tidy no go.mod/go.sum diff.
tkkhq added 6 commits July 8, 2026 22:57
Replaces the GitHub-release-based update-notice check (VOL-168) with the
Volcano Hosting API as the source of truth: the CLI reports its version on
every API request, and acts on whatever instruction the API returns, instead
of independently polling GitHub.

- internal/api: versionProtocolDoer wraps every HTTP call (including
  unauthenticated ones like device/token exchange) to set
  X-Volcano-CLI-Version and a real User-Agent (volcano-cli/<version>
  (<os>/<arch>), replacing Go's default), and to record
  X-Volcano-CLI-Instruction / X-Volcano-CLI-Latest-Version /
  X-Volcano-Device-Instruction response headers via the new
  api.LastInstructions(). One process runs one command, so this is safe as
  process-global, mutex-guarded state.
- internal/cmd/upgrade: removed the GitHub-polling notice machinery
  (MaybePrintUpdateNotice + on-disk cache) entirely; added
  PrintAPIInstructionNotices, which renders a non-blocking upgrade
  suggestion or a deprecation warning from api.LastInstructions() with zero
  extra network round-trips (it only reads headers the command's own API
  calls already received). `volcano upgrade`'s actual download/checksum/
  cosign-verify flow (internal/update) is unchanged \u2014 GitHub remains the
  download source, only the "should I nudge/require upgrade" decision moves
  to the API.
- cmd/volcano/main.go: prints notices unconditionally after Execute()
  (cobra skips PersistentPostRun on a RunE error, so this can't live there);
  a 426 (version_deprecation) error gets the concrete upgrade target
  appended when the API provided one; any other error gets a re-auth hint
  when the API signaled device_instruction=reauth.

Security note: the claimed version has no cryptographic binding to a real
release (volcano-cli is open source; per-version keypairs/checksums were
considered and rejected \u2014 see volcano-hosting docs/cli/version-gating.md).
This is intentional: the header is advisory input the CLI cooperates with,
never a privilege, so spoofing it can only escape a restriction, never
escalate one.

Server-side counterpart: volcano-hosting#522 (merged after this),
volcano-hosting#532 (dormant policy-arming staging PR).
Rename to leave room for other instruction categories:
- suggestion_upgrade -> suggestion_version_upgrade
- version_deprecation -> require_version_upgrade

Constant names follow: CLIInstructionSuggestionVersionUpgrade,
CLIInstructionRequireVersionUpgrade. Server counterpart: volcano-hosting#522.
Mirrors volcano-hosting's CLIInstructionLowCreditWarning /
CLIInstructionNotEnoughCredit: reserved, unimplemented instruction values
locked in now so a future billing-service integration doesn't need a
wire-format rename on either side. No case for these in
PrintAPIInstructionNotices yet \u2014 the API never emits them, and CLI-side
handling needs its own design pass once billing integration is scoped.
…tion (VOL-180)

Adds real, testable handling for the reserved not_enough_credit /
low_credit_warning instructions in PrintAPIInstructionNotices \u2014 not just
the constants. Unreached today since the API never emits these (see
volcano-hosting's matching creditgate scaffold), but once it does, this
side needs zero changes to start working.

The printed notice is a placeholder, not the designed UX: marckong's
actual ask is an interactive prompt ("upgrade?" / "purchase extra
credits?") that takes a real action, which needs a target (URL? command?)
that doesn't exist yet. internal/confirm already has the prompt primitive
for that once there's something concrete to confirm \u2014 noted inline so the
next change is obvious.
All 6 findings verified against the actual code before fixing.

1. [P3] Fixed a stale comment claiming PrintAPIInstructionNotices has no
   case for the reserved credit instructions \u2014 the credit-hook commit
   added exactly those cases, but the comment written before that landed
   was never updated.

2. [P3] recordInstructions previously overwrote the full Instructions
   snapshot on every response, so a command making multiple API calls
   could have a real notice (suggestion, deprecation, or reauth) silently
   dropped by a later, unrelated response that simply didn't repeat the
   header. Now merges per-field: CLIInstruction and its paired
   LatestVersion update together (they come from the same server-side gate
   decision), DeviceInstruction updates independently, and neither is
   cleared by an empty value \u2014 only ever replaced by a new non-empty one.
   Added ResetLastInstructionsForTest (exported, test-only) since the
   sticky behavior means tests can no longer rely on an unrelated response
   to reset state between cases.

3. [nit] main's non-blocking-error path printed the pending notice before
   the actual "Error:" line. Reordered so the failure always prints first
   \u2014 both for humans skimming stderr and for log parsers/scripts that
   assume line 1 is the error.

4. [nit] Extracted main()'s orchestration (426 short-circuit, error-before-
   notice ordering, exit codes) into a testable run(root, deps) int
   function, and added tests driving it through real commands against a
   test server (success/no-notice, success/suggestion, 426 short-circuit
   without duplicating the notice, non-blocking error ordering, reauth
   ordering) \u2014 previously only the extracted print helpers were tested,
   not this orchestration itself.

5. [nit] Fixed a comment that wrapped api.CLIInstructionNotEnoughCredit
   across two lines mid-identifier, reading like a typo.

6. [nit] Removed the now-unused update.Notice type, the last remnant of
   the removed GitHub-polling notice model.

Verified: go build/vet clean, full test suite green (including -count=5
-shuffle=on on the changed package to rule out the new sticky-state
cross-test contamination this surfaced and fixed), make lint 0 issues,
make tidy no go.mod/go.sum diff.
)

The prior run() tests covered the 426 (blocking) deprecation path and the
suggestion notice on success, but not the exact scenario
printDeprecationWarning's own doc comment calls out as the reason the
notice-after-success print exists at all: a deprecated CLI succeeding on
an exempt route (e.g. login), where the server sets require_version_upgrade
without a 426. Only PrintAPIInstructionNotices' isolated unit test covered
that instruction value before \u2014 not the full run() orchestration (exit
code 0, notice still printed).

Verified: full suite green, -count=5 -shuffle=on on cmd/volcano clean,
make lint 0 issues, make tidy no go.mod/go.sum diff.
@tkkhq tkkhq force-pushed the feat/api-version-protocol branch from 0a6894a to 8fd1132 Compare July 9, 2026 02:57
Sign up for free to 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.

2 participants