Skip to content

feat(devices): send this install's current name on the heartbeat - #264

Merged
stormer78 merged 1 commit into
mainfrom
feat/device-name-heartbeat
Aug 29, 2026
Merged

feat(devices): send this install's current name on the heartbeat#264
stormer78 merged 1 commit into
mainfrom
feat/device-name-heartbeat

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

The client half of verifiable-trust-infrastructure#1191, unblocked by #263.

The gap

displayName is written once, at registration, and device/register is intentionally refused from the second launch on (device/register:alreadyRegistered) — so nothing this install could do would ever change it. Rename the machine, or run the install under a different profile, and its binding goes on identifying it by a name that no longer picks it out of the list the name exists for.

The change

The heartbeat carries the current name through device_heartbeat_named. The VTA applies it only when it differs, and only to the binding the caller authenticated as, so this cannot touch another device's row.

Two decisions worth stating:

Sent on every beat, not only when we think it changed. This side cannot know what the maintainer holds without asking. The payload is one short string on a five-minute timer, and a correction that depends on a local belief is one that stays wrong exactly when the belief is.

A drift seen in the listing is corrected immediately, not on the next beat. Not an optimisation — the name only travels on a heartbeat, so an install opened and closed inside one interval would never send one and the stale name would survive every launch. That is precisely the case a rename produces: you rename the machine, restart, and look at the list. The listing is already fetched for the sibling check and our own row already identified there by consumerDid, so this costs nothing new on the wire.

Tests

name_correction_due is a pure predicate, so the drift cases are pinned directly: a renamed host, a switched profile (the name is host and profile, so both drift it), a binding the VTA returned with no name at all, and — the one that keeps the steady state quiet — a name that already matches, which must not fire a heartbeat on every listing.

Full sweep: cargo fmt, clippy --workspace --all-targets, cargo test --workspace -- --include-ignored, RUSTDOCFLAGS="-D warnings" cargo doc.

Pre-merge checklist

- [x] No new reqwest::Client::new() / bare fetch(); all clients have finite timeouts (R1.2) — reuses the presence task's client; the trust task keeps the SDK's DEVICE_TT_TIMEOUT
- [x] No lock held across a network await (R1.3)
- [x] No local state committed before its remote effect, or the flow is resumable with an idempotency key (R2.1) — nothing is persisted locally; the name is re-sent every beat, so a lost correction self-heals
- [x] Every retry is bounded + backed off; non-idempotent ops are not blind-retried (R1.4) — the immediate correction is one attempt, and a failure falls through to the ordinary interval rather than looping
- [x] Accept/poll/listen loops survive transient errors (R1.5) — a failed correction is logged at debug and the loop continues; the heartbeat's own failure handling is unchanged
- [x] Acks/deletes happen only after durable handoff (R1.6) — n/a
- [x] New/changed wire types: camelCase, deny_unknown_fields where security-relevant, schema registered, all consumers (incl. JS) updated (R3.*) — none authored here; the `ext` member and its key are defined in vta-sdk and validated against the generated ExtKey pattern on the VTA side
- [x] Config absence = most restrictive; fail-closed if enforcement can't start (R5.*) — a VTA that does not understand the extension ignores it and the name stays as it was
- [x] Logs/status claim only what was verified; background-job failures are surfaced (R6.*) — the correction logs at debug either way; nothing claims the name was applied, because only the VTA decides that
- [x] "Process dies on the next line" answered for every mutation touched (R2.1) — the only mutation is at the VTA, and it is idempotent: the next beat sends the same name
- [x] Deviations from this guide flagged explicitly with rule numbers — none

The client half of VTI #1191, now that vta-sdk 0.32 is in.

`displayName` is written once, at registration, and `device/register` is
intentionally refused from the second launch on — so nothing this install
could do would ever change it. Rename the machine, or run the install under a
different profile, and the binding went on identifying it by a name that no
longer picks it out of the list the name exists for.

The heartbeat now carries the current name through
`device_heartbeat_named`. The VTA applies it only when it differs, and only
to the binding the caller authenticated as, so this cannot touch another
device's row.

Sent on every beat rather than only when we believe it changed: this side
cannot know what the maintainer holds without asking, the payload is one
short string on a five-minute timer, and a correction that depends on a local
belief is one that stays wrong exactly when the belief is.

A drift seen in the listing is corrected immediately rather than waited out.
That is not an optimisation: the name only travels on a heartbeat, so an
install opened and closed inside one interval would never send one, and the
stale name would survive every launch — the case a rename is most likely to
produce.

The listing is already fetched for the sibling check, and our own row is
already identified there by `consumerDid`, so noticing the drift costs
nothing new on the wire.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 requested a review from a team as a code owner August 29, 2026 02:30
@stormer78
stormer78 merged commit 2aa7ce3 into main Aug 29, 2026
14 checks passed
@stormer78
stormer78 deleted the feat/device-name-heartbeat branch August 29, 2026 02:34
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.

1 participant