Skip to content

feat(credentials): enforce and surface lock-advertised PIN length bounds#1279

Open
raman325 wants to merge 2 commits into
mainfrom
feat/credential-length-capabilities
Open

feat(credentials): enforce and surface lock-advertised PIN length bounds#1279
raman325 wants to merge 2 commits into
mainfrom
feat/credential-length-capabilities

Conversation

@raman325

Copy link
Copy Markdown
Owner

Proposed change

Derive the tightest-common (min, max) PIN length across all of a slot's bound locks, then enforce it on write and surface it in the UI.

  • Authoritative gate (slot coordinator): a non-empty PIN is validated against every bound lock's advertised length range before it is written. A violation raises ServiceValidationError naming each offending lock and its required range. Empty PINs (slot clears) are exempt. Locks whose capabilities are not cached (disconnected or not yet probed) and locks that do not advertise the credential type are skipped, so the write proceeds rather than blocking on unknown limits and the sync layer surfaces any later device rejection.
  • Best-effort hints (PIN text entity): native_min/native_max now reflect the live tightest-common range across the bound locks. Non-credential keys (the slot name) and an unsatisfiable intersection fall back to the default range so the control is never rendered inverted, and the range always widens to admit the current stored value (the empty string after a clear, or a PIN written before the lock advertised its limits) so HA state rendering never raises. The hints do not block input; the coordinator gate is what rejects out-of-range PINs.

New building blocks:

  • LockCapabilities.length_bounds(credential_type) — effective (min, max) for one lock, normalizing non-positive advertised bounds to "unbounded"/"no minimum".
  • aggregate_length_bounds(...) — folds many locks into one tightest-common range (largest min, smallest max); UI defaults deliberately live in the caller.
  • BaseLock.cached_capabilities — synchronous, I/O-free read of the warmed capability cache for synchronous callers.

This is groundwork that generalizes cleanly to other credential types: CREDENTIAL_TYPE_BY_CONF_KEY in text.py is the single per-type knob, and the helpers are credential-type-parametric.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New feature (which adds functionality)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

  • This PR fixes or closes issue: fixes #
  • This PR is related to issue:

Copilot AI review requested due to automatic review settings June 20, 2026 18:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added python Pull requests that update Python code enhancement New feature or request labels Jun 20, 2026
@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.86364% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 97.13%. Comparing base (5f5163c) to head (ac2213c).
⚠️ Report is 9 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...nents/lock_code_manager/domain/slot_coordinator.py 95.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1279      +/-   ##
==========================================
+ Coverage   97.10%   97.13%   +0.03%     
==========================================
  Files          54       54              
  Lines        6553     6638      +85     
  Branches      460      460              
==========================================
+ Hits         6363     6448      +85     
  Misses        190      190              
Flag Coverage Δ
python 97.66% <98.86%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...components/lock_code_manager/domain/credentials.py 100.00% <100.00%> (ø)
...om_components/lock_code_manager/providers/_base.py 97.27% <100.00%> (+0.21%) ⬆️
custom_components/lock_code_manager/text.py 98.68% <100.00%> (+1.62%) ⬆️
...nents/lock_code_manager/domain/slot_coordinator.py 95.09% <95.00%> (-0.02%) ⬇️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

raman325 and others added 2 commits June 21, 2026 22:09
Derive the tightest-common (min, max) PIN length across all bound locks
and use it in two places:

- The slot coordinator validates a non-empty PIN against every bound
  lock's advertised range before writing. This is the authoritative gate
  and raises ServiceValidationError naming each offending lock with its
  required range. Empty PINs (slot clears) are exempt, and locks whose
  capabilities are not cached (disconnected or unprobed) fail open so the
  sync layer surfaces any later device rejection.
- The PIN text entity surfaces the live bounds as native_min/native_max
  hints, falling back to the default range for non-credential keys or an
  unsatisfiable intersection, and always widening to admit the current
  stored value so HA state rendering never raises.

Adds LockCapabilities.length_bounds, the aggregate_length_bounds helper,
and BaseLock.cached_capabilities (a synchronous, I/O-free read of the
warmed capability cache).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 180b413b60c6
Address code-review findings on the length-bounds feature:

- Stop surfacing the advertised minimum as native_min. Home Assistant's
  text.set_value validates len(value) < min before the coordinator, which
  blocked the empty string that clears a slot and pre-empted the per-lock
  error. native_min stays permissive; the coordinator is the authoritative
  minimum gate. The maximum is still surfaced as a hard ceiling.
- Probe newly added locks in the background and re-push state so native_max
  reflects them instead of waiting for an unrelated write.
- Restore @Final on _get_cached_capabilities (lost when cached_capabilities
  was inserted above it).
- Compute the bound once per render now that native_min is constant.
- Document the shared entry-wide lock set used by the gate and the entity.
- Make the "0 means unbounded/unknown" length convention explicit on
  CredentialTypeCapability.

Tests: boundary lengths, the unbounded-max message branch, removal reverting
bounds, and a service-level regression proving a PIN clears when a lock
advertises a positive minimum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: de2e84619c12
@raman325 raman325 force-pushed the feat/credential-length-capabilities branch from a2ab72f to ac2213c Compare June 22, 2026 02:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request lcm-minor Minor version bump python Pull requests that update Python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants