ktalk-cli 4.0.0__tar.gz → 4.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/PKG-INFO +1 -1
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/pyproject.toml +1 -1
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/cli_token.py +5 -4
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/fs_guard.py +18 -3
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/token_file.py +67 -20
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_secret_masking.py +1 -1
- ktalk_cli-4.0.1/tests/test_windows_native_auth.py +522 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_write_journal.py +2 -2
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/.gitignore +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/LICENSE +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/README.md +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/CLAUDE.md +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/__init__.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/auth.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/calendar_reader.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/cli.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/cli_config.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/cli_contacts.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/cli_content.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/cli_doctor.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/cli_meeting.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/cli_meeting_args.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/cli_meeting_confirm.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/cli_meetings_read.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/cli_sanction.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/cli_store.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/cli_sync.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/client.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/config.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/confirmation.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/contacts.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/contour_diagnostics.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/download.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/endpoints.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/enrichment.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/formatters.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/host_config.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/meeting_body.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/meeting_cancel.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/meeting_scheduling.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/pagination.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/reconciliation.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/registry.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/rooms.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/store.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/store_migration.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/talk_config.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/transcript_identity.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/write_journal.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/src/ktalk_cli/write_sanction.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/conftest.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/access-info-expired.json +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/access-info-full.json +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/archive-8col.md +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/archive-dup-id.md +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/archive-escaped-pipe-7col.md +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/archive-page1.json +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/archive-page2-empty.json +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/archive-pipe-in-name.md +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/calendar-item-session.json +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/chat-messages-session.json +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/conference-history-session-oversized.json +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/conference-history-session.json +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/error-body-problem-json.json +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/error-body-validation.json +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/recording-detail-session-full-participants.json +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/recording-detail-session-oversized-partial.json +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/recording-detail-with-qualities.json +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/recording-list-item-session.json +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/recordings-list-apikey-page1.json +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/recordings-list-apikey-page2.json +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/registry-archive-2026-04.md +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/registry.md +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/fixtures/room-detail-session.json +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_adr022_ktalk_cli_rename.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_archive.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_auth_modes.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_calendar.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_calendar_window_iso8601.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_chat.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_cli.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_cli_config_set.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_cli_config_show.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_cli_content.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_cli_export_store_isolation.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_cli_host_config_wiring.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_cli_meeting.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_cli_meeting_sanctioned.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_cli_meetings_surface.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_cli_migrate_to_central_store.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_client.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_code_review_dev003.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_concurrency.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_config.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_confirmation.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_contour_diagnostics.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_diagnostics.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_download.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_enrichment.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_formatters.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_fr19_auth_status.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_fr21_no_vault_layout.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_fr39_calendar_inclusive_end.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_fr40_timezone_format.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_fr41_last_synced.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_fr42_single_credential_source.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_fr43_legacy_key_removal_warning.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_fr44_auth_status_accepted_rejected.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_fr47_missing_fields_completeness.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_fr48_doctor_summary_diagnostics.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_host_config.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_issue3_null_id_fallback.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_legacy_config_detection.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_meeting_body.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_meeting_cancel.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_meeting_scheduling.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_migration.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_nfr17_identity_verification.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_pagination.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_reconciliation.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_registry.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_rooms.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_search_contacts.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_sec001_control_char.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_sec001_legacy_var_401.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_sec001_symlink_guard.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_settings_config_sources.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_store.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_store_migration.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_sync_honest_window.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_talk_config.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_token_file.py +0 -0
- {ktalk_cli-4.0.0 → ktalk_cli-4.0.1}/tests/test_write_sanction.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ktalk-cli
|
|
3
|
-
Version: 4.0.
|
|
3
|
+
Version: 4.0.1
|
|
4
4
|
Summary: CLI for accessing Kontur Talk (KTalk) recordings, transcripts and summaries
|
|
5
5
|
Project-URL: Homepage, https://github.com/mdemyanov/ktalk-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/mdemyanov/ktalk-cli
|
|
@@ -65,10 +65,11 @@ def _cmd_status(args: argparse.Namespace) -> int:
|
|
|
65
65
|
return EXIT_OK
|
|
66
66
|
print(f"present: True, права {mode}")
|
|
67
67
|
if token is None:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
# Совет приходит из `status_payload` — пара (причина × платформа):
|
|
69
|
+
# chmod существует только на платформах с выразимыми POSIX-битами и
|
|
70
|
+
# только для причины «права шире» (ADR-028 §3).
|
|
71
|
+
advice = payload["advice"] or "запишите токен: ktalk token set -"
|
|
72
|
+
print(f"usable: False — {advice}")
|
|
72
73
|
return EXIT_OK
|
|
73
74
|
print(f"usable: True, {_mask(token)}")
|
|
74
75
|
return EXIT_OK
|
|
@@ -26,14 +26,29 @@ def open_temp_exclusive(tmp: Path, mode: int) -> int:
|
|
|
26
26
|
`O_NOFOLLOW`/`O_EXCL` — симлинк, подложенный на место `.tmp` между
|
|
27
27
|
проверкой и открытием, оставался рабочим путём записи.
|
|
28
28
|
|
|
29
|
-
`O_NOFOLLOW` отказывается идти по уже существующему
|
|
30
|
-
|
|
29
|
+
`O_NOFOLLOW` отказывается идти по уже существующему симлинку; подставляется
|
|
30
|
+
`getattr(os, "O_NOFOLLOW", 0)`, а не константой: атрибут есть не на всех
|
|
31
|
+
платформах (нативный Windows его не имеет — ADR-028 §1), и безусловная
|
|
32
|
+
константа роняла обе команды записи `AttributeError`-ом ещё до записи
|
|
33
|
+
значения. На POSIX `getattr` возвращает ту же константу — набор флагов
|
|
34
|
+
бит-в-бит прежний. Отсутствие `O_NOFOLLOW` защиты не снимает: `O_CREAT|O_EXCL`
|
|
35
|
+
отказывает на любом уже существующем `.tmp`, включая подложенный симлинк —
|
|
36
|
+
для create-new ссылка не преследуется ни на POSIX (`O_EXCL`, POSIX.1-2008),
|
|
37
|
+
ни на Windows (`CREATE_NEW`); ветка повтора снимает остаток `unlink`-ом,
|
|
38
|
+
который никогда не идёт по симлинку, и вторая попытка снова под `O_EXCL` —
|
|
39
|
+
выигравший гонку атакующий получает отказ, тихой записи по подмене нет.
|
|
40
|
+
Остаточная роль `O_NOFOLLOW` — различие errno (ELOOP против EEXIST) на
|
|
41
|
+
существующей записи; создать симлинк на Windows можно только с привилегией
|
|
42
|
+
(`SeCreateSymbolicLinkPrivilege` либо DevMode) — у непривилегированного
|
|
43
|
+
локального атакующего из модели угроз хода нет.
|
|
44
|
+
|
|
45
|
+
`O_EXCL` отказывается, если `.tmp` уже существует (обычный файл — остаток
|
|
31
46
|
прерванной записи, либо симлинк, подложенный другим локальным
|
|
32
47
|
пользователем) — обрабатывается только безобидная ветка: остаток
|
|
33
48
|
удаляется (`unlink` никогда не идёт по симлинку) и попытка повторяется
|
|
34
49
|
один раз; если атакующий выиграл гонку заново, вторая попытка тоже
|
|
35
50
|
проваливается — тихого успеха через подмену нет."""
|
|
36
|
-
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os
|
|
51
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
|
|
37
52
|
try:
|
|
38
53
|
return os.open(tmp, flags, mode)
|
|
39
54
|
except FileExistsError:
|
|
@@ -12,7 +12,13 @@
|
|
|
12
12
|
|
|
13
13
|
Права шире `0600` читаются как «файла нет» — тот же fail-closed барьер, что у
|
|
14
14
|
санкции записи (SEC-006). Это защита не от владельца учётной записи, а от другого
|
|
15
|
-
пользователя машины, которому файл стал доступен.
|
|
15
|
+
пользователя машины, которому файл стал доступен. Барьер применяется только там,
|
|
16
|
+
где POSIX-битам есть смысл (`os.name != "nt"`, ADR-028 §2): нативный `stat` на
|
|
17
|
+
Windows сообщает `0o666` для любого обычного файла, барьер отвергал бы там каждый
|
|
18
|
+
рукописный файл независимо от содержимого; границей защиты на этой платформе
|
|
19
|
+
служит изоляция профиля пользователя, и инструмент не посылает файлу явного
|
|
20
|
+
послабления. На обеих платформах пригодность означает корректную форму значения
|
|
21
|
+
(`_TOKEN_RE`), а не любое непустое содержимое (FR-51).
|
|
16
22
|
"""
|
|
17
23
|
|
|
18
24
|
from __future__ import annotations
|
|
@@ -42,40 +48,81 @@ def token_path() -> Path:
|
|
|
42
48
|
return root / "ktalk-mcp" / "token"
|
|
43
49
|
|
|
44
50
|
|
|
45
|
-
def
|
|
46
|
-
"""
|
|
51
|
+
def _verdict() -> tuple[str | None, str | None, str | None]:
|
|
52
|
+
"""Вердикт одним проходом: `(токен, права, причина отказа)`. Права — строка
|
|
53
|
+
вида `0600` для диагностики, `None`, если файла нет. Причина — `None` для
|
|
54
|
+
пригодного файла, иначе `not_present` / `wide_mode` / `unreadable` / `empty`
|
|
55
|
+
/ `bad_content`; её читает совет диагностики (`status_payload`, ADR-028 §2-3).
|
|
56
|
+
|
|
57
|
+
Платформенная развилка читает `os.name` в момент вызова, а не на импорте:
|
|
58
|
+
ось тестов (NFR-26) подменяет атрибут в окне вызова."""
|
|
59
|
+
path = token_path()
|
|
47
60
|
try:
|
|
48
|
-
|
|
61
|
+
st_mode = stat.S_IMODE(path.stat().st_mode)
|
|
49
62
|
except OSError:
|
|
50
|
-
return None
|
|
63
|
+
return None, None, "not_present"
|
|
64
|
+
mode = format(st_mode, "04o")
|
|
65
|
+
if os.name != "nt" and st_mode & 0o077:
|
|
66
|
+
return None, mode, "wide_mode"
|
|
67
|
+
try:
|
|
68
|
+
raw = path.read_text(encoding="utf-8")
|
|
69
|
+
except (OSError, ValueError, UnicodeDecodeError):
|
|
70
|
+
return None, mode, "unreadable"
|
|
71
|
+
token = raw.strip()
|
|
72
|
+
if not token:
|
|
73
|
+
return None, mode, "empty"
|
|
74
|
+
if not _TOKEN_RE.match(token):
|
|
75
|
+
return None, mode, "bad_content"
|
|
76
|
+
return token, mode, None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def file_mode() -> str | None:
|
|
80
|
+
"""Права файла как `0600` — для диагностики. `None`, если файла нет."""
|
|
81
|
+
return _verdict()[1]
|
|
51
82
|
|
|
52
83
|
|
|
53
84
|
def read_token() -> str | None:
|
|
54
|
-
"""Значение токена или `None` — если файла нет, он нечитаем,
|
|
55
|
-
|
|
85
|
+
"""Значение токена или `None` — если файла нет, он нечитаем, пуст, его
|
|
86
|
+
содержимое не по форме токена или (везде, кроме родной Windows) его права
|
|
87
|
+
шире `0600`. Ни одна из этих причин не является ошибкой: вызывающая
|
|
56
88
|
сторона просто переходит к следующему источнику (или к отказу конфигурации)."""
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
89
|
+
return _verdict()[0]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _remediation_advice(cause: str | None) -> str | None:
|
|
93
|
+
"""Совет для причины «не пригоден» — пара (причина × платформа), собирается
|
|
94
|
+
здесь же, чтобы `cli_token` и `doctor` печатали одно и то же (ADR-026,
|
|
95
|
+
ADR-028 §3): chmod-совет существует только там, где POSIX-биты выразимы, и
|
|
96
|
+
только для причины «права шире» — на родной Windows он невыполним и не
|
|
97
|
+
печатается."""
|
|
98
|
+
if cause is None:
|
|
63
99
|
return None
|
|
64
|
-
|
|
65
|
-
|
|
100
|
+
if cause == "wide_mode" and os.name != "nt":
|
|
101
|
+
return f"права шире 0600; почините: chmod 600 {token_path()}"
|
|
102
|
+
if cause == "empty":
|
|
103
|
+
return "файл пуст; запишите токен заново: ktalk token set -"
|
|
104
|
+
if cause == "bad_content":
|
|
105
|
+
return (
|
|
106
|
+
"содержимое не похоже на токен (одно слово из букв и цифр, "
|
|
107
|
+
"16-64 символа); запишите заново: ktalk token set -"
|
|
108
|
+
)
|
|
109
|
+
return "запишите токен: ktalk token set -"
|
|
66
110
|
|
|
67
111
|
|
|
68
112
|
def status_payload() -> dict:
|
|
69
|
-
"""
|
|
70
|
-
|
|
113
|
+
"""Путь/права/пригодность/причина одним словарём — единственная точка сборки,
|
|
114
|
+
которую переиспользуют `cli_token.py::_cmd_status` и `cli_doctor.py`
|
|
71
115
|
(ADR-026 spec §2: значение совпадает буквальным переиспользованием кода, не
|
|
72
|
-
соглашением между двумя независимо написанными местами).
|
|
73
|
-
|
|
116
|
+
соглашением между двумя независимо написанными местами). `cause`/`advice` —
|
|
117
|
+
пара (причина × платформа) из ADR-028 §3."""
|
|
118
|
+
token, mode, cause = _verdict()
|
|
74
119
|
return {
|
|
75
120
|
"path": str(token_path()),
|
|
76
121
|
"present": mode is not None,
|
|
77
122
|
"mode": mode,
|
|
78
|
-
"usable":
|
|
123
|
+
"usable": token is not None,
|
|
124
|
+
"cause": cause,
|
|
125
|
+
"advice": _remediation_advice(cause),
|
|
79
126
|
}
|
|
80
127
|
|
|
81
128
|
|