Skip to content

release-train: staging -> main - #627

Open
LukasWodka wants to merge 2 commits into
mainfrom
release-train/to-main
Open

release-train: staging -> main#627
LukasWodka wants to merge 2 commits into
mainfrom
release-train/to-main

Conversation

@LukasWodka

@LukasWodka LukasWodka commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Automated promotion by the release train (RFC-0008 D14). Head is the train-managed release-train/to-main branch (a mirror of staging), so it never collides with a human PR. Merged only when the fr-gate is green.


Note

High Risk
Changes safety behavior for destructive cluster mutations (ingest, delete, resources set, seal); scripted --no-input runs without anchor or API reachability will now fail instead of mutating an unverified target.

Overview
CLI 0.10.21 tightens mutating cluster commands so they no longer warn-and-proceed when the target cannot be verified (backend#2983). Instead they verify identity via the API (kube-system UID → account client) and print that name/namespace, or refuse before any write unless the operator passes --i-know-the-target.

The cluster guard now distinguishes unreadable ID, known-wrong anchor (ack flag does not bypass), API-verified proceed, offline anchor match, and unverifiable fail-closed—with HTTP 426 hard-stopping and Ctrl-C exiting quietly during verify. --i-know-the-target is wired through data ingest, data delete, resources set, and client status --seal (with validation that cluster flags only apply with --seal).

Extensive tests cover ingest/delete pre-flight refusal, ack semantics, and updated help golden files.

Reviewed by Cursor Bugbot for commit 7abd211. Bugbot is set up for automated code reviews on this repo. Configure here.

aptracebloc and others added 2 commits September 1, 2026 18:02
… when it can't be (backend#2983) (#620)

* fix(cluster): verify the mutating target from the API and fail closed when it can't be (backend#2983)

A machine with no recorded cluster anchor warned "the target couldn't be
verified ... Proceeding against https://127.0.0.1:8444" and then mutated
anyway. The endpoint it named is a localhost port-forward — tbcluster
allocates 8443/8444/8445 across fleets, so the same command shape reaches a
different fleet depending on which tunnel is up on which port. The operator's
only signal about which environment was about to change was a port number, and
--no-input (contract: "fail on missing required values") did NOT fail closed:
a CI job could ingest into the wrong fleet and get a green exit.

guardActiveClientCluster now does both of the issue's directions:

  1. VERIFY, don't merely record. It asks the API which client owns the
     cluster actually reached (keyed on the live kube-system UID) and prints
     that identity — name + namespace — instead of a port. This does not depend
     on local state, which is exactly what was missing in the field.
  2. FAIL CLOSED when unverifiable. If the API can't confirm the target and no
     recorded anchor matches it, the guard refuses (exit non-zero, no write)
     unless the operator passes --i-know-the-target. A scripted --no-input
     caller has no human to ask, so this honours its contract; a human gets the
     same named escape hatch.

The backend#2863 wrong-cluster refusal and its offline anchor path are kept:
an anchor MISMATCH is still a hard refusal (--i-know-the-target does not
override a known-wrong target, only an unverifiable one), and an anchor that
MATCHES the live cluster still proceeds offline.

--i-know-the-target is added to every command that mutates through
resolveClusterTarget (data ingest, data delete, resources set,
client status --seal), so the safety check is uniform and never a dead end.

Tests: an unverifiable target under --no-input fails closed (data ingest and
the destructive data delete paths); the printed identity comes from the API,
not local state; the escape hatch proceeds; the anti-rot call-site sweep is
updated for the new argument.

VERSION 0.10.20 -> 0.10.21: 0.10.20 is already released, so this first
published-path PR of the cycle carries the pending bump the version-bump-gate
requires.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cluster): hard-stop a 426 in the target verifier, don't swallow it (backend#2983)

Bugbot: verifyTargetFromAPI collapsed EVERY ListClients failure to
"unverifiable", including a 426 UpgradeRequiredError. So a too-old CLI got
waved through — an anchored machine proceeded via the offline anchor-match, and
an unanchored one failed closed with a "no client registered — run client
create" message instead of "tracebloc upgrade". The repo's learned rule is that
HTTP 426 must be a hard failure, not a warning.

verifyTargetFromAPI now returns the 426 (and only the 426) as an error; every
other lookup failure stays swallowed to "couldn't confirm". The guard propagates
it as a hard exit (exitFailure) BEFORE the anchor-match and fail-closed paths,
so a stale local anchor can't press on past it. --i-know-the-target does not
override it — upgrading is the only way through, and UpgradeRequiredError.Error()
already says so.

Tests (also covers two hardening asks from review):
  - 426 + a MATCHING anchor still hard-stops with the upgrade message (would
    otherwise have proceeded via anchor-match).
  - wrong-cluster mismatch + --i-know-the-target STILL refuses (the flag
    overrides an unknown target, never a known-wrong one) — mutation-proof for
    the security asymmetry.
  - unreadable-id + --i-know-the-target proceeds with a warning.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cluster): distinguish "couldn't reach the API" from "no client registered" (backend#2983)

Bugbot (#515 shape): verifyTargetFromAPI returned the same "unverifiable" for an
unreachable/unsigned-in API and for a successful list with no matching cluster_id.
The fail-closed message then asserted "tracebloc has no client registered for it"
and recommended `client create` in BOTH cases — reporting a couldn't-confirm as a
confirmed absence, and pointing `client create` at an unconfirmed cluster (the
adopt-vs-mint hazard the codebase guards against elsewhere via three-valued reads).

verifyTargetFromAPI now returns (client, reached, err), mirroring
DiscoverInClusterClientID's three-valued contract and clientSurvey.looked:
  - reached=true, client!=nil  → verified.
  - reached=true, client==nil  → CONFIRMED absence: the "no client registered — run
    client create" refusal is honest here (client create adopts an existing client
    on the cluster, or mints for a new one).
  - reached=false              → couldn't ask (offline / not signed in / transient):
    the refusal says tracebloc couldn't be reached and points at sign-in / --context,
    and never asserts absence or recommends client create.
  - 426 still propagates as the hard-stop error (unchanged).

Tests: split the fail-closed coverage into an unreachable case (asserts it does NOT
say "no client registered" / "client create") and a confirmed-absence case (asserts
it does). Golden updated for the two messages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cluster): don't assert absence when the on-cluster client just isn't linked (backend#2983)

Bugbot: the reached=true refusal said "tracebloc has no client registered for it"
whenever no account client was ANCHORED to the live UID. But by the time the guard
runs, resolveClusterTarget has already discovered a client release on this cluster —
so it is never truly absent — and a legacy client's ClusterID is empty, so it can
never match by UID however present it is. Asserting absence there is a couldn't-tell
dressed as confirmed.

Reworded the reached=true branch: the on-cluster client "isn't linked to any client
in your account (it may be newly installed, or created before cluster anchoring)",
and `tracebloc client create` "adopts the client already on this cluster and records
it". No false absence, and the advice is exactly the adopt+backfill path. Guard
behaviour (fail closed) is unchanged — only the wording.

Test renamed ConfirmedAbsence → ReachedButUnmatched, now includes a legacy
empty-ClusterID record in the account list and asserts the refusal does NOT say
"has no client registered". Golden updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cluster): treat a Ctrl-C during target verify as a quiet interrupt, not a refusal (backend#2983)

Bugbot: verifyTargetFromAPI folds every non-426 lookup error into "couldn't ask"
(reached=false), including context.Canceled. So a Ctrl-C during the new ~8s backend
lookup became reached=false — and on a MATCHING recorded anchor the guard then
AUTHORIZED the mutation the operator just aborted; with no anchor it exited 3 with a
"couldn't be reached / run login" refusal instead of a quiet 130, and later cluster
calls failed under the already-cancelled context with a misleading kubeconfig error.

Added interrupted(ctx): if the command's context is cancelled during a blocking
guard step it returns exitInterrupted (130). It checks the context itself, so it is
not fooled by verifyTargetFromAPI's inner verify timeout (which cancels only its own
derived context) — a merely-slow backend still reads as reached=false. Wired at both
blocking points: after the identity read and after the backend lookup, the latter
BEFORE the anchor-match, so an abort can never fall through to a proceed.

Test: a cancelled context during verify, with a matching anchor, exits 130 and does
NOT print the anchor-match proceed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cluster): keep the Ctrl-C guard exit silent (backend#2983)

Bugbot (Low): interrupted() attached ctx.Err() to the exitError, so IsSilentError
was false and main() printed "Error: context canceled" on a Ctrl-C during the verify
steps. exitInterrupted is meant to be a SILENT exit (IsSilentError keys on err==nil),
which every other interrupt path relies on (auth.go, seal.go, client_status.go all
return a bare &exitError{code: exitInterrupted}).

Drop the inner err. Test now also asserts IsSilentError so the convention can't
regress.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 7abd211. Configure here.

@tracebloc-release-train tracebloc-release-train Bot added the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gate-nudge Toggled by the release train to (re-)fire the fr-gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants