s-leasekit
Единый верификатор лиза способности — один и тот же код проверки на обоих
потребителях: skillery-cli (запуск навыка, демон) и gateway (load_capability).
Версия: 0.1.0 · Лицензия: MIT · Зависимости: cryptography (Ed25519) — и всё.
Реализует контракт лиза способности (внутренний документ
capability-lease-contract.md, 2026-08-06): TTL 24 ч, grace = 0, гейт включается
точечно (requires_lease), NO_GRANT/GRANT_REVOKED наружу не различаются.
Зачем отдельный пакет
skillkit — библиотека CLI, adapterkit — библиотека gateway; общей у них нет. Две
независимые реализации проверки подписи и срока разъедутся — вопрос не «если», а
«когда». И разъедутся они в сторону «пускаем»: лишний отказ замечают в тот же
день, лишний пропуск не замечает никто. Поэтому реализация одна, и она здесь.
Быстрый старт
from pathlib import Path
from leasekit import lease_gate, LeaseDenied
try:
lease = lease_gate(
"grok_transcriber", # имя = ключ entry-point skillery.plugins
store=Path.home() / ".skillery", # каталог состояния
subject=current_user_subject, # "user:1042" — текущая учётка
device_id=client_device_id, # справочно: расхождение = WARN
hub_offset=hub_time_offset, # из заголовка Date последнего ответа хаба
)
except LeaseDenied as denied:
log.warning("lease denied: %s", denied.verdict.value)
raise SystemExit(denied.message) # текст уже содержит следующее действие
# сюда попадаем ТОЛЬКО с проверенным лизом
run_capability(lease.claims.cap)
Где ставить гейт (§9.2 контракта):
- CLI —
commands/run.py::cmd_run, перед_spawn(argv); - gateway —
gateway/plugins.py::load_capability, передep.load().
Перечисление способностей гейтом не закрывается: список того, что установлено на машине, секретом не является.
Почему проверку нельзя обойти по невнимательности
Форма публичного API — не вкусовщина, а способ убрать целые классы ошибок:
| Решение | Что оно предотвращает |
|---|---|
verify_lease / lease_gate поднимают LeaseDenied, а не возвращают вердикт |
проигнорированный return-код молча пускает; проигнорированное исключение останавливает процесс |
«Проверить» и «получить claims» — одно действие: LeaseClaims достаётся только из VerifiedLease |
нельзя разобрать payload, «забыв» проверить его; функции «просто распарсить» в публичном API нет |
subject — обязательный именованный аргумент без значения по умолчанию |
единственную привязку, по которой отказывают (§2.4), нельзя пропустить |
Время берётся из LeaseClock, а не из now: int |
применение монотонного пола — обязанность кита, а не вызывающего |
DEVICE_MISMATCH — не вердикт, а LeaseWarning |
естественный код if verdict is not VALID: deny не начнёт отказывать там, где спека велит пропускать |
Диагностический путь называется inspect_lease и возвращает другой тип (LeaseCheck) |
«взять не ту функцию» не выглядит как рабочий код: LeaseCheck возвращается и при отказе |
Любая неоднозначность → отказ (MALFORMED) |
битый JSON, обрезанный файл, чужой алгоритм, отсутствующий claim не превращаются в молчаливый пропуск |
Монотонный пол времени
Лиз проверяется по локальным часам — других на офлайн-машине нет. Три правила §5, и без всех трёх контур дырявый:
- допуск ±300 с на
nbf/exp— обычный перекос часов не даёт отказа на границе; - смещение от часов хаба —
offset = hub_now − local_nowиз заголовкаDateпоследнего успешного ответа; спасает машины с систематически сбитыми часами (VM без NTP, севшая CMOS) без ослабления допуска; - монотонный пол
hub_time_floor— максимальное время, когда-либо подтверждённое хабом.
effective_now = max(local_now + offset, hub_time_floor) # LeaseClock.effective_now
Пол только растёт (LeaseStore.raise_floor никогда не уменьшает) и живёт в
leases.json рядом с лизами. Без него весь контур снимается одной командой
«перевести часы назад»: истёкший лиз оживает, и срок доступа перестаёт что-либо
значить.
Две ситуации разделены намеренно, хотя обе — отказ:
- часы ниже пола сверх допуска →
CLOCK_SUSPECT(«похоже, часы идут неверно»); - лиз истёк, а часы правдоподобны →
EXPIRED(«срок истёк, подключитесь к сети»).
Отказ с неверной причиной отправляет пользователя искать несуществующий отзыв доступа.
Вердикты
| Вердикт | Поведение |
|---|---|
VALID |
пропустить, в сеть не ходить |
MISSING |
лиза нет в хранилище |
EXPIRED |
exp (+grace) позади |
NOT_YET_VALID |
nbf впереди сверх допуска |
SUBJECT_MISMATCH |
лиз выдан другому пользователю |
CAPABILITY_MISMATCH |
лиз выписан на другую способность |
ISSUER_MISMATCH |
другой хаб (проверяется, только если issuer задан) |
SIGNATURE_INVALID |
подпись не сходится или alg ≠ EdDSA (включая none) |
UNKNOWN_KID |
ключ неизвестен; с refresh_jwks — ровно один повтор, иначе отказ |
UNSUPPORTED_VER |
ver лиза новее клиента |
CLOCK_SUSPECT |
часы ниже пола или неправдоподобно впереди |
MALFORMED |
битый токен/JSON/нет обязательного claim |
Предупреждения (LeaseWarning, не влияют на решение): DEVICE_MISMATCH, NO_TIME_FLOOR.
Что кит НЕ делает — и почему
- Не ходит в сеть. Ни за лизом, ни за JWKS. У CLI и gateway разные транспорты, таймауты и политика ретраев; тащить сюда сетевой слой значило бы тащить и его зависимости обоим потребителям.
- Не выдаёт лизы. Единственная точка решения «право действует» — хаб
(
is_grant_effective). Модульleasekit.testingподписывает лизы только для тестов — вынесен в отдельный модуль, чтобы не попадаться в подсказках IDE в проде. - Не решает, когда удалять лиз. Различение «хаб ответил нет» (удалить немедленно)
и «хаб недоступен» (жить по имеющемуся лизу до
exp) требует знания об ответе сети — это знает потребитель. Кит лишь выполняетdrop()корректно и атомарно. - Не хранит гранты и не знает про биллинг. Лиз — булево «можно», а не счётчик и не квота: счётчик требует онлайна на каждый вызов, то есть ровно того, от чего отказались.
- Не зависит от
skillkit/adapterkit. Он должен ставиться обоим потребителям независимо; общая зависимость снизу — ровно то, ради чего кит существует. - Не защищает от владельца машины. Файлы способности лежат на диске, интерпретатор открыт, проверку можно вырезать из своей копии клиента. Лиз делает честного клиента честным и ставит праву конечный срок — не более.
- Не гейтит перечисление установленного и не отзывается точечно (CRL нет, §6.2).
Расхождения со спекой
Спека писалась до кода; ниже — где реализация от неё отходит и почему.
- Сигнатуры публичных функций. Спека §3.1 обещает
verify_lease(...) -> LeaseVerdictиlease_gate(...) -> None. Реализовано: обе поднимаютLeaseDeniedи возвращаютVerifiedLease. Возврат вердикта-энума — это return-код, который можно не проверить; в контуре, где «лишний пропуск никто не заметит», такой API работает против собственной цели. Диагностический возврат вердикта сохранён под другим именем —inspect_lease() -> LeaseCheck. now: int→clock: LeaseClock. Голоеnowперекладывает применение монотонного пола на вызывающего, то есть на два места, которые разъедутся.DEVICE_MISMATCH— не вердикт, а предупреждение (обоснование выше).- Добавлены вердикты
MALFORMED,CAPABILITY_MISMATCH,NOT_YET_VALID,ISSUER_MISMATCH. Спека §9.2 их не перечисляет, но требует fail-closed на любую нераспознанную ситуацию; отдельные коды нужны, чтобы отказ объяснял причину. lease_gate(subject=...)обязателен. В спеке подпись гейта субъекта не содержит — ноSUBJECT_MISMATCHбез субъекта непроверяем, а «взять текущую учётку самому» кит не может: он не знает, где она лежит у каждого потребителя.- Хранилище (
LeaseStore) вошло в кит, хотя §3.1 упоминает только две функции. Форматleases.jsonи дисциплина «пол только растёт» — ровно то, что разъедется в двух копиях. Политика удаления при этом осталась у потребителей. refresh_jwksкак callback. Спека требует «при живой сети обновить JWKS и повторить проверку ОДИН раз». Сеть — не дело кита, но правило «ровно один повтор» держится здесь, а не в двух копиях.
Тесты
uv sync --extra dev
uv run pytest -q
Ключевые случаи — не формальность, а то, ради чего контур существует:
перевод часов назад не оживляет истёкший лиз; расхождение client_device_id даёт
предупреждение, а не отказ; подделанная подпись, alg: none и неизвестный kid
отвергаются.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file s_leasekit-0.1.2.tar.gz.
File metadata
- Download URL: s_leasekit-0.1.2.tar.gz
- Upload date:
- Size: 71.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
22c3d2d6b84e2fa3b9f5817038ff2c3d007294688e601a37c32607001ee56d96
|
|
| MD5 |
0c78fe6465194cadfda97285b2cdbcde
|
|
| BLAKE2b-256 |
49fe9f28f073a14991944b909673eb5055631beca9c66193e44f53edb2cf5d58
|
File details
Details for the file s_leasekit-0.1.2-py3-none-any.whl.
File metadata
- Download URL: s_leasekit-0.1.2-py3-none-any.whl
- Upload date:
- Size: 30.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5af5b3dd38e4ab1ed782ef1ef4f0be2ea0d3e0f6ce7f6b0bb9e7910bc5b4879e
|
|
| MD5 |
29a6911b87ea6e51032cfb003f7f6572
|
|
| BLAKE2b-256 |
4c02451a559394301eb1192c36713f18fcb127a3f39b9bd97371881031a2b1e1
|