netizen-cli 0.11.0__tar.gz → 0.11.2__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.
Files changed (257) hide show
  1. {netizen_cli-0.11.0/netizen_cli.egg-info → netizen_cli-0.11.2}/PKG-INFO +3 -2
  2. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/README.md +15 -8
  3. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/config.example.yaml +6 -5
  4. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/__init__.py +1 -1
  5. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/auth.py +99 -169
  6. netizen_cli-0.11.2/netizen_cli/admin/credentials.py +233 -0
  7. netizen_cli-0.11.2/netizen_cli/admin/feishu.py +335 -0
  8. netizen_cli-0.11.2/netizen_cli/admin/login.py +234 -0
  9. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/port_config.py +2 -2
  10. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/presentation.py +23 -0
  11. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/queries.py +45 -3
  12. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/static/admin.css +110 -25
  13. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/static/admin.js +597 -346
  14. netizen_cli-0.11.2/netizen_cli/admin/static/chat-picker.css +37 -0
  15. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/static/chat-picker.js +131 -50
  16. netizen_cli-0.11.2/netizen_cli/admin/static/identity.js +272 -0
  17. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/static/index.html +165 -89
  18. netizen_cli-0.11.2/netizen_cli/admin/static/login.html +29 -0
  19. netizen_cli-0.11.2/netizen_cli/admin/static/login.js +134 -0
  20. netizen_cli-0.11.2/netizen_cli/admin/users.py +378 -0
  21. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/web.py +327 -114
  22. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/bindings.py +105 -16
  23. netizen_cli-0.11.2/netizen_cli/cards/chat_target.py +283 -0
  24. netizen_cli-0.11.2/netizen_cli/cards/fork.py +350 -0
  25. netizen_cli-0.11.2/netizen_cli/cards/pagination.py +50 -0
  26. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/reply.py +122 -42
  27. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/scheduled.py +441 -189
  28. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/reply_presenter.py +200 -50
  29. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel_app.py +350 -86
  30. netizen_cli-0.11.2/netizen_cli/chat_avatars.py +114 -0
  31. netizen_cli-0.11.2/netizen_cli/chat_targets.py +24 -0
  32. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cli.py +76 -20
  33. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cli_data.py +2 -5
  34. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cli_services.py +0 -1
  35. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cli_setup.py +77 -17
  36. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cli_update.py +25 -6
  37. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cli_update_worker.py +71 -13
  38. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/codex_runtime.py +129 -44
  39. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/defaults/service.py +9 -1
  40. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/domain.py +9 -0
  41. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/feishu_app_onboarding.py +7 -3
  42. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/feishu_app_permissions.py +18 -15
  43. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/main.py +29 -33
  44. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/management/__init__.py +2 -0
  45. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/management/chat_directory.py +53 -20
  46. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/management/service.py +259 -36
  47. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/management/updates.py +26 -0
  48. netizen_cli-0.11.2/netizen_cli/pagination.py +46 -0
  49. netizen_cli-0.11.2/netizen_cli/partial_answers.py +54 -0
  50. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/runtime/contracts.py +14 -6
  51. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/schedules/service.py +43 -18
  52. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/sdk_gap_adapter.py +15 -0
  53. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/service_launcher.py +0 -5
  54. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/settings.py +2 -26
  55. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/terminal_cleanup.py +2 -2
  56. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/turn_activity.py +16 -2
  57. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/turn_files.py +11 -22
  58. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/turn_plan_observer.py +8 -2
  59. {netizen_cli-0.11.0 → netizen_cli-0.11.2/netizen_cli.egg-info}/PKG-INFO +3 -2
  60. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli.egg-info/SOURCES.txt +22 -0
  61. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli.egg-info/requires.txt +2 -1
  62. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/pyproject.toml +3 -2
  63. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/skills/netizen-user-guide/references/user-guide.md +136 -48
  64. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_bindings.py +202 -1
  65. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cards.py +141 -0
  66. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_channel_app.py +4 -3
  67. netizen_cli-0.11.2/tests/test_chat_avatars.py +133 -0
  68. netizen_cli-0.11.2/tests/test_chat_target_cards.py +181 -0
  69. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli.py +126 -1
  70. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli_data.py +20 -1
  71. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli_integration.py +28 -5
  72. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli_setup.py +106 -4
  73. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli_update.py +163 -6
  74. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_codex_runtime.py +369 -8
  75. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_codex_sdk_capabilities.py +1 -1
  76. netizen_cli-0.11.2/tests/test_cron_chat_targets.py +461 -0
  77. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_deployment.py +2 -2
  78. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_feishu_app_onboarding.py +4 -2
  79. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_feishu_app_permissions.py +23 -7
  80. netizen_cli-0.11.2/tests/test_fork_cards.py +533 -0
  81. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_main.py +44 -25
  82. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_migration_schema_validation.py +47 -0
  83. netizen_cli-0.11.2/tests/test_pagination.py +75 -0
  84. netizen_cli-0.11.2/tests/test_partial_answer_delivery.py +487 -0
  85. netizen_cli-0.11.2/tests/test_partial_answers.py +54 -0
  86. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_persistent_fork_bindings.py +16 -0
  87. netizen_cli-0.11.2/tests/test_persistent_fork_channel.py +842 -0
  88. netizen_cli-0.11.2/tests/test_probe_feishu_partial_answers.py +138 -0
  89. netizen_cli-0.11.2/tests/test_probe_persistent_fork.py +42 -0
  90. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_probe_scheduled_tasks.py +1 -0
  91. netizen_cli-0.11.2/tests/test_probe_skill_execution.py +167 -0
  92. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_schedule_card_retry.py +56 -1
  93. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_schedule_cards.py +355 -10
  94. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_schedule_scope_matrix.py +3 -1
  95. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_schedule_service.py +109 -1
  96. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_scheduled_channel.py +107 -0
  97. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_sdk_gap_adapter.py +47 -2
  98. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_service_launcher.py +103 -8
  99. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_session_defaults.py +24 -0
  100. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_settings_projects.py +12 -43
  101. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_terminal_cleanup.py +2 -2
  102. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_turn_activity.py +31 -1
  103. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_turn_plan_observer.py +21 -1
  104. netizen_cli-0.11.0/netizen_cli/admin/static/chat-picker.css +0 -19
  105. netizen_cli-0.11.0/netizen_cli/cards/fork.py +0 -327
  106. netizen_cli-0.11.0/tests/test_fork_cards.py +0 -244
  107. netizen_cli-0.11.0/tests/test_persistent_fork_channel.py +0 -450
  108. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/MANIFEST.in +0 -0
  109. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/_build.py +0 -0
  110. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/__main__.py +0 -0
  111. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/account_rate_limits.py +0 -0
  112. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/__init__.py +0 -0
  113. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/errors.py +0 -0
  114. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/transport.py +0 -0
  115. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/builtin_skills.py +0 -0
  116. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/__init__.py +0 -0
  117. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/callbacks.py +0 -0
  118. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/controls.py +0 -0
  119. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/defaults.py +0 -0
  120. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/model_info.py +0 -0
  121. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/questions.py +0 -0
  122. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/usage.py +0 -0
  123. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/__init__.py +0 -0
  124. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/completion_mentions.py +0 -0
  125. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/input_preparation.py +0 -0
  126. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/messages.py +0 -0
  127. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/ports.py +0 -0
  128. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/question_inputs.py +0 -0
  129. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/reactions.py +0 -0
  130. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/topics.py +0 -0
  131. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cli_packages.py +0 -0
  132. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cli_update_restore.py +0 -0
  133. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/completion_mention.py +0 -0
  134. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/database_migrations.py +0 -0
  135. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/defaults/__init__.py +0 -0
  136. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/defaults/models.py +0 -0
  137. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/defaults/store.py +0 -0
  138. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/deployment/__init__.py +0 -0
  139. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/deployment/restart_worker.py +0 -0
  140. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/deployment/update_executor.py +0 -0
  141. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/deployment/update_protocol.py +0 -0
  142. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/error_messages.py +0 -0
  143. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/experience.py +0 -0
  144. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/feishu_links.py +0 -0
  145. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/git_status.py +0 -0
  146. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/image_inputs.py +0 -0
  147. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/instance.py +0 -0
  148. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/lark_app.py +0 -0
  149. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/management/blocking_io.py +0 -0
  150. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/management/chat_labels.py +0 -0
  151. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/management/coordination.py +0 -0
  152. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/markdown_images.py +0 -0
  153. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/message_content.py +0 -0
  154. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/message_history.py +0 -0
  155. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/message_preparation.py +0 -0
  156. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/message_projection.py +0 -0
  157. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/migrations/__init__.py +0 -0
  158. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/migrations/schema.py +0 -0
  159. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/migrations/v14.py +0 -0
  160. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/model_settings.py +0 -0
  161. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/package_resources.py +0 -0
  162. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/projects.py +0 -0
  163. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/prompt_projection.py +0 -0
  164. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/quoted_context.py +0 -0
  165. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/result_images.py +0 -0
  166. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/runtime/__init__.py +0 -0
  167. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/runtime/name_writes.py +0 -0
  168. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/runtime/thread_naming.py +0 -0
  169. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/schedules/__init__.py +0 -0
  170. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/schedules/mcp.py +0 -0
  171. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/schedules/models.py +0 -0
  172. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/schedules/scheduler.py +0 -0
  173. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/schedules/store.py +0 -0
  174. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/session_settings.py +0 -0
  175. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/skill_references.py +0 -0
  176. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/task_text.py +0 -0
  177. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/turn_patch_children.py +0 -0
  178. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/user_questions.py +0 -0
  179. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli.egg-info/dependency_links.txt +0 -0
  180. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli.egg-info/entry_points.txt +0 -0
  181. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli.egg-info/top_level.txt +0 -0
  182. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/setup.cfg +0 -0
  183. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/skills/netizen-lark/SKILL.md +0 -0
  184. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/skills/netizen-user-guide/SKILL.md +0 -0
  185. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_account_rate_limits.py +0 -0
  186. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_admin_update_executor_probe.py +0 -0
  187. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_binding_scheduled_channel.py +0 -0
  188. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_channel_fixtures.py +0 -0
  189. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_channel_input_preparation.py +0 -0
  190. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_check_sdk.py +0 -0
  191. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli_distribution.py +0 -0
  192. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli_packages.py +0 -0
  193. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli_packaging.py +0 -0
  194. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli_services.py +0 -0
  195. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_codex_sdk_upgrade_0161.py +0 -0
  196. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_completion_mention_cards.py +0 -0
  197. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_completion_mentions.py +0 -0
  198. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_completion_topic_delivery.py +0 -0
  199. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_database_migrations.py +0 -0
  200. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_defaults_cards.py +0 -0
  201. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_defaults_channel.py +0 -0
  202. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_defaults_runtime.py +0 -0
  203. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_documentation_links.py +0 -0
  204. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_domain_experience.py +0 -0
  205. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_error_messages.py +0 -0
  206. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_file_pagination.py +0 -0
  207. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_git_status.py +0 -0
  208. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_image_inputs.py +0 -0
  209. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_lark_app.py +0 -0
  210. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_lark_sdk_contract.py +0 -0
  211. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_markdown_images.py +0 -0
  212. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_message_content.py +0 -0
  213. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_message_history.py +0 -0
  214. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_message_material_inputs.py +0 -0
  215. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_message_preparation.py +0 -0
  216. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_model_info_cards.py +0 -0
  217. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_model_settings.py +0 -0
  218. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_name_writes.py +0 -0
  219. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_persistent_fork_runtime.py +0 -0
  220. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_probe_child_files.py +0 -0
  221. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_probe_feishu_completion_mention.py +0 -0
  222. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_probe_feishu_turn_file_card.py +0 -0
  223. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_probe_python_sdk.py +0 -0
  224. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_probe_sdk_completion_race.py +0 -0
  225. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_probe_thread_naming.py +0 -0
  226. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_project_delete_channel.py +0 -0
  227. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_project_delete_runtime.py +0 -0
  228. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_project_deletion_storage.py +0 -0
  229. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_prompt_projection.py +0 -0
  230. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_question_cards.py +0 -0
  231. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_question_channel.py +0 -0
  232. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_question_target_parity.py +0 -0
  233. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_quoted_context.py +0 -0
  234. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_release_script.py +0 -0
  235. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_repository_hygiene.py +0 -0
  236. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_result_images.py +0 -0
  237. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_resume_channel.py +0 -0
  238. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_retired_deployment.py +0 -0
  239. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_runtime_resume_activation.py +0 -0
  240. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_runtime_thread_naming.py +0 -0
  241. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_schedule_mcp.py +0 -0
  242. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_schedule_run_now.py +0 -0
  243. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_scheduled_runtime.py +0 -0
  244. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_scheduler.py +0 -0
  245. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_scheduler_presenter.py +0 -0
  246. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_schedules_storage.py +0 -0
  247. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_session_settings.py +0 -0
  248. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_task_text.py +0 -0
  249. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_thread_naming.py +0 -0
  250. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_turn_files.py +0 -0
  251. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_turn_patch_children.py +0 -0
  252. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_turn_patch_statistics.py +0 -0
  253. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_update_executor.py +0 -0
  254. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_usage_card.py +0 -0
  255. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_usage_control.py +0 -0
  256. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_user_guide_skill.py +0 -0
  257. {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_user_questions.py +0 -0
@@ -1,16 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netizen-cli
3
- Version: 0.11.0
3
+ Version: 0.11.2
4
4
  Summary: Use native Codex threads through a Feishu Channel.
5
5
  Requires-Python: <3.15,>=3.11
6
6
  Requires-Dist: h11==0.16.0
7
+ Requires-Dist: httpx==0.28.1
7
8
  Requires-Dist: lark-channel-sdk==1.4.0
8
9
  Requires-Dist: lark-oapi==1.7.2
9
10
  Requires-Dist: markdown-it-py==4.0.0
10
11
  Requires-Dist: mdformat==1.0.0
11
12
  Requires-Dist: mdformat-gfm==1.0.0
12
13
  Requires-Dist: mcp==2.2.0
13
- Requires-Dist: openai-codex==0.161.0
14
+ Requires-Dist: openai-codex==0.162.1
14
15
  Requires-Dist: pydantic==2.13.4
15
16
  Requires-Dist: PyYAML==6.0.3
16
17
  Requires-Dist: qrcode==8.2
@@ -83,16 +83,20 @@ netizen start
83
83
  netizen status
84
84
  ```
85
85
 
86
- setup 准备配置、飞书授权和实例数据并注册服务,不默认启动。首次缺凭据会输出浏览器
87
- 验证链接;Agent 可运行 `netizen setup </dev/null`,转交链接并保留同一进程,确认后自动
88
- 保存凭据。不要把 App Secret 发到聊天里。start 等待服务真正就绪,每次实际启动均检查
89
- 数据格式并按需迁移。
86
+ setup 准备配置、飞书应用、首位管理员和实例数据并注册服务,不默认启动。缺应用凭据或
87
+ 管理员时会输出浏览器验证链接;Agent 可运行 `netizen setup </dev/null`,逐次转交链接并
88
+ 保留同一进程,确认后自动保存。已有应用也可用 `netizen setup --admin-open-id ou_...`
89
+ 指定该应用下的首位管理员;已有名单不会被覆盖。不要把 App Secret 发到聊天里。
90
+ start 等待服务真正就绪,每次实际启动均检查管理员配置、数据格式并按需迁移;后台启动
91
+ 缺配置会明确失败,不等待扫码。
90
92
 
91
93
  默认实例数据集中在 `~/.netizen`;程序位于选定 Python 环境。用 `--root` 或 NETIZEN_ROOT
92
94
  选择其他实例,例如 `netizen setup --root "$HOME/work/.netizen" --admin-port 8890`。
93
95
  各实例共享原生 Codex 状态;服务固定注册时的 Python,终端换 venv 不改变已有绑定。
94
96
 
95
97
  日常维护用 `netizen update` 更新当前环境并恢复原来运行的实例;它不接受 --root。
98
+ Agent/脚本使用 `netizen update --json`:stdout 只输出最终 JSON,进度和包管理器输出
99
+ 写入 stderr。以 JSON 和退出码判断结果,包验证成功与实例恢复 ready 分别报告。
96
100
  `netizen remove --root ...` 默认保留数据,--purge 才清理精确范围,-y 只省略确认。
97
101
  完整流程、失败边界、跨环境切换和卸载见 [CLI 安装与维护](docs/cli.md)。
98
102
 
@@ -162,10 +166,13 @@ Side 会在同一聊天中新建话题,可继续多轮讨论。它与原会话
162
166
  ## 管理与维护
163
167
 
164
168
  实例管理员可以通过 **Admin Web** 集中管理项目、会话、Side 话题、会话默认配置和定时任务,
165
- 并在“系统维护”中检查正式更新或重启服务。发送 `/admin` 可获取当前实例的管理 URL
169
+ 并在“系统维护”中查看安装信息或重启服务。发送 `/admin` 可获取当前实例的管理 URL
166
170
  和根目录,无需创建会话。未指定端口时首次从 `8787` 起分配并保存,各实例独立监听;
167
- 管理页仍使用安装器生成的独立管理员凭据,命令不会返回凭据或免登录链接。
168
- [访问方式](docs/deployment.md#配置与管理页访问)见部署文档。
171
+ 管理页使用飞书登录,并核对当前应用的管理员名单,命令不会返回凭据或免登录链接。
172
+ Admin 是实例必备能力;管理员可在“系统管理 → 权限管理 → 管理员”中维护名单。
173
+ 服务重启后需要重新登录。
174
+ [访问方式](docs/deployment.md#配置与管理页访问)见部署文档。飞书登录与管理员维护描述
175
+ 当前源码行为,真人和平台验收尚未完成,见[验收状态](docs/deployment.md#admin-飞书登录与管理员维护验收)。
169
176
 
170
177
  升级与重启都会影响正在执行的任务,重启后不会自动续跑。
171
178
  [正式升级、启停与卸载](docs/deployment.md#升级启停和卸载) ·
@@ -174,7 +181,7 @@ Side 会在同一聊天中新建话题,可继续多轮讨论。它与原会话
174
181
  ## 使用前了解
175
182
 
176
183
  - **共享环境。** 飞书应用的可用范围和群成员关系控制谁能访问;同一实例复用服务账号,
177
- 不提供按用户或项目隔离的权限体系。Admin Web 面向受信内网的单管理员。
184
+ 不提供按用户或项目隔离的权限体系。Admin Web 面向受信内网,名单中的管理员权限相同。
178
185
  - **输入与输出。** 支持文本、普通图片和富文本图片,也可提供飞书 2.0 卡片、合并转发与转发话题作为
179
186
  [背景材料](skills/netizen-user-guide/references/user-guide.md#卡片合并转发与转发话题)。材料内部的媒体只提供未读附件描述;直接文件和音视频仍不支持。
180
187
  飞书 1.0 卡片明确不支持。
@@ -15,10 +15,9 @@ projects:
15
15
  channel:
16
16
  securityMode: audit
17
17
 
18
- # Single-administrator management UI. It is enabled by default and listens on
19
- # all interfaces for direct access inside the trusted private network.
18
+ # Required management UI, using this app's Feishu administrator identities.
19
+ # It listens on all interfaces for direct access inside the trusted private network.
20
20
  adminWeb:
21
- enabled: true
22
21
  host: 0.0.0.0
23
22
  # Omit port for first-start allocation from 8787 through 8886. Once bound,
24
23
  # the selected port is written here and stays fixed on subsequent starts.
@@ -31,5 +30,7 @@ adminWeb:
31
30
  # The installer prepares this file; see docs/deployment.md for its JSON format.
32
31
  # An explicit override must be an absolute path. Lark CLI is optional.
33
32
  # NETIZEN_LARK_APP_CONFIG=/home/your-user/.netizen/lark-app/config.json
34
- # Admin Web supports only a canonical 32-byte base64url credential file.
35
- # NETIZEN_ADMIN_SECRET_FILE=/home/your-user/.netizen/credentials/admin-web-secret
33
+ # Setup binds the first administrator through Feishu login or --admin-open-id.
34
+ # Administrator membership: <NETIZEN_ROOT>/state/admin-users.json
35
+ # Private user credentials: <NETIZEN_ROOT>/credentials/admin-user-tokens.json
36
+ # Admin browser sessions stay in memory and require login after service restart.
@@ -1,3 +1,3 @@
1
1
  """Netizen: a thin Feishu Channel for native Codex Threads."""
2
2
 
3
- __version__ = "0.11.0"
3
+ __version__ = "0.11.2"
@@ -1,7 +1,7 @@
1
- """In-memory authentication state for the single-administrator Admin Web.
1
+ """In-memory browser authentication and authorization for Admin Web.
2
2
 
3
- This module deliberately contains no HTTP concepts. It owns the credential
4
- generation, opaque browser tokens, login throttling, and one-shot action
3
+ This module deliberately contains no HTTP concepts. It owns opaque browser
4
+ tokens, login throttling, and one-shot action
5
5
  authorization state; the HTTP adapter is responsible for cookies, Host,
6
6
  Origin, and status-code mapping.
7
7
  """
@@ -14,23 +14,19 @@ import hashlib
14
14
  import hmac
15
15
  import ipaddress
16
16
  import math
17
- import os
18
17
  import secrets
19
- import stat
20
- import threading
21
18
  import time
22
19
  from collections import deque
23
20
  from collections.abc import Callable
24
21
  from dataclasses import dataclass, field, fields, is_dataclass
25
22
  from enum import Enum
26
- from pathlib import Path
27
23
  from typing import Generic, TypeVar, cast
28
24
 
25
+ from .users import AdminIdentity, AdminUsers, AdminUsersUnavailable
26
+
29
27
 
30
28
  _TOKEN_BYTES = 32
31
29
  _TOKEN_TEXT_BYTES = 43
32
- _MAX_CREDENTIAL_FILE_BYTES = _TOKEN_TEXT_BYTES + 1
33
- _DUMMY_DIGEST = hashlib.sha256(bytes(_TOKEN_BYTES)).digest()
34
30
 
35
31
 
36
32
  class AdminAuthError(RuntimeError):
@@ -39,13 +35,6 @@ class AdminAuthError(RuntimeError):
39
35
  code = "admin_auth_error"
40
36
 
41
37
 
42
- class CredentialFileError(AdminAuthError):
43
- code = "credential_unavailable"
44
-
45
- def __init__(self) -> None:
46
- super().__init__("admin credential unavailable")
47
-
48
-
49
38
  class AdmissionClosed(AdminAuthError):
50
39
  code = "admin_admission_closed"
51
40
 
@@ -168,27 +157,6 @@ class AuthLimits:
168
157
  raise ValueError("per-session action capacity exceeds global capacity")
169
158
 
170
159
 
171
- @dataclass(frozen=True, slots=True)
172
- class CredentialIdentity:
173
- """Identity fields whose legal change constitutes credential rotation."""
174
-
175
- device: int
176
- inode: int
177
- mode: int
178
- links: int
179
- owner: int
180
- group: int
181
- size: int
182
- modified_ns: int
183
- changed_ns: int
184
-
185
-
186
- @dataclass(frozen=True, slots=True)
187
- class CredentialSnapshot:
188
- identity: CredentialIdentity
189
- digest: bytes = field(repr=False)
190
-
191
-
192
160
  @dataclass(frozen=True, slots=True)
193
161
  class IssuedPreauthChallenge:
194
162
  cookie_token: str = field(repr=False)
@@ -200,12 +168,16 @@ class IssuedSession:
200
168
  token: str = field(repr=False)
201
169
  log_handle: str
202
170
  generation: int
171
+ identity: AdminIdentity
172
+ add_revision: int
203
173
 
204
174
 
205
175
  @dataclass(frozen=True, slots=True)
206
176
  class AuthenticatedSession:
207
177
  log_handle: str
208
178
  generation: int
179
+ identity: AdminIdentity
180
+ add_revision: int
209
181
 
210
182
 
211
183
  class ExpectationMode(str, Enum):
@@ -264,6 +236,8 @@ class RedeemedAction(Generic[TargetT, PreconditionsT]):
264
236
  action_kind: str
265
237
  target: TargetT
266
238
  preconditions: PreconditionsT
239
+ identity: AdminIdentity
240
+ add_revision: int
267
241
 
268
242
 
269
243
  @dataclass(frozen=True, slots=True)
@@ -289,6 +263,8 @@ class _SessionRecord:
289
263
  source: str
290
264
  log_handle: str
291
265
  generation: int
266
+ identity: AdminIdentity
267
+ add_revision: int
292
268
 
293
269
 
294
270
  @dataclass(slots=True, repr=False)
@@ -321,86 +297,7 @@ def normalize_source_ip(source: str) -> str:
321
297
  return address.compressed.lower()
322
298
 
323
299
 
324
- def load_credential_snapshot(path: str | Path) -> CredentialSnapshot:
325
- """Read and validate the credential without following the final symlink.
326
-
327
- The two metadata snapshots and the content read all use the same file
328
- descriptor. The returned object contains only a digest, never credential
329
- text or decoded secret bytes.
330
- """
331
-
332
- credential_path = os.fspath(path)
333
- if not os.path.isabs(credential_path):
334
- raise CredentialFileError()
335
- nofollow = getattr(os, "O_NOFOLLOW", None)
336
- cloexec = getattr(os, "O_CLOEXEC", None)
337
- if not nofollow or cloexec is None:
338
- raise CredentialFileError()
339
- flags = os.O_RDONLY | nofollow | cloexec
340
- try:
341
- descriptor = os.open(credential_path, flags)
342
- except OSError:
343
- raise CredentialFileError() from None
344
- try:
345
- try:
346
- before = os.fstat(descriptor)
347
- _validate_credential_metadata(before)
348
- content = _read_bounded(descriptor, _MAX_CREDENTIAL_FILE_BYTES)
349
- after = os.fstat(descriptor)
350
- _validate_credential_metadata(after)
351
- except OSError:
352
- raise CredentialFileError() from None
353
- before_identity = _credential_identity(before)
354
- after_identity = _credential_identity(after)
355
- if before_identity != after_identity:
356
- raise CredentialFileError()
357
- digest = _credential_digest(content, allow_terminal_lf=True)
358
- if digest is None:
359
- raise CredentialFileError()
360
- return CredentialSnapshot(identity=after_identity, digest=digest)
361
- finally:
362
- os.close(descriptor)
363
-
364
-
365
- def _validate_credential_metadata(metadata: os.stat_result) -> None:
366
- if not stat.S_ISREG(metadata.st_mode):
367
- raise CredentialFileError()
368
- if stat.S_IMODE(metadata.st_mode) != 0o600:
369
- raise CredentialFileError()
370
- if metadata.st_size < _TOKEN_TEXT_BYTES:
371
- raise CredentialFileError()
372
- if metadata.st_size > _MAX_CREDENTIAL_FILE_BYTES:
373
- raise CredentialFileError()
374
-
375
-
376
- def _credential_identity(metadata: os.stat_result) -> CredentialIdentity:
377
- return CredentialIdentity(
378
- device=metadata.st_dev,
379
- inode=metadata.st_ino,
380
- mode=metadata.st_mode,
381
- links=metadata.st_nlink,
382
- owner=metadata.st_uid,
383
- group=metadata.st_gid,
384
- size=metadata.st_size,
385
- modified_ns=metadata.st_mtime_ns,
386
- changed_ns=metadata.st_ctime_ns,
387
- )
388
-
389
-
390
- def _read_bounded(descriptor: int, maximum: int) -> bytes:
391
- content = bytearray()
392
- while len(content) <= maximum:
393
- chunk = os.read(descriptor, maximum + 1 - len(content))
394
- if not chunk:
395
- return bytes(content)
396
- content.extend(chunk)
397
- raise CredentialFileError()
398
-
399
-
400
- def _credential_digest(content: bytes, *, allow_terminal_lf: bool) -> bytes | None:
401
- token = content
402
- if allow_terminal_lf and token.endswith(b"\n"):
403
- token = token[:-1]
300
+ def _opaque_digest(token: bytes) -> bytes | None:
404
301
  if len(token) != _TOKEN_TEXT_BYTES:
405
302
  return None
406
303
  if b"\n" in token or b"\r" in token:
@@ -424,7 +321,7 @@ def _token_digest(token: object) -> bytes | None:
424
321
  encoded = token.encode("ascii")
425
322
  except UnicodeEncodeError:
426
323
  return None
427
- return _credential_digest(encoded, allow_terminal_lf=False)
324
+ return _opaque_digest(encoded)
428
325
 
429
326
 
430
327
  def _new_token() -> tuple[str, bytes]:
@@ -440,18 +337,15 @@ class AdminAuth:
440
337
 
441
338
  def __init__(
442
339
  self,
443
- credential_path: str | Path,
340
+ users: AdminUsers,
444
341
  *,
445
342
  clock: Callable[[], float] = time.monotonic,
446
343
  limits: AuthLimits | None = None,
447
344
  ) -> None:
448
- snapshot = load_credential_snapshot(credential_path)
449
- self._credential_path = Path(credential_path)
345
+ self._users = users
450
346
  self._clock = clock
451
347
  self._limits = AuthLimits() if limits is None else limits
452
- self._lock = threading.RLock()
453
- self._credential_digest: bytes | None = snapshot.digest
454
- self._credential_identity: CredentialIdentity | None = snapshot.identity
348
+ self._lock = users.lock
455
349
  self._generation = 1
456
350
  self._admission_open = True
457
351
  self._last_now: float | None = None
@@ -460,6 +354,11 @@ class AdminAuth:
460
354
  self._actions: dict[bytes, _ActionRecord] = {}
461
355
  self._rate_by_source: dict[str, deque[float]] = {}
462
356
  self._rate_global: deque[float] = deque()
357
+ users.subscribe_revocation(self.revoke_identity)
358
+
359
+ @property
360
+ def users(self) -> AdminUsers:
361
+ return self._users
463
362
 
464
363
  @property
465
364
  def generation(self) -> int:
@@ -471,15 +370,9 @@ class AdminAuth:
471
370
  with self._lock:
472
371
  return self._admission_open
473
372
 
474
- def refresh_credential(self) -> bool:
475
- """Validate the credential path and apply a legal rotation atomically."""
476
-
477
- with self._lock:
478
- return self._refresh_credential_locked()
479
-
480
373
  def issue_preauth(self, source_ip: str) -> IssuedPreauthChallenge:
481
374
  with self._lock:
482
- self._refresh_credential_locked()
375
+ self._require_admission_locked()
483
376
  now = self._now_locked()
484
377
  self._prune_locked(now)
485
378
  source = self._normalize_login_source(source_ip)
@@ -510,18 +403,17 @@ class AdminAuth:
510
403
  form_nonce=form_nonce,
511
404
  )
512
405
 
513
- def login(
406
+ def consume_preauth(
514
407
  self,
515
408
  *,
516
409
  source_ip: str,
517
410
  cookie_token: object,
518
411
  form_nonce: object,
519
- credential: object,
520
- ) -> IssuedSession:
521
- """Consume one pre-auth challenge and exchange the credential for a session."""
412
+ ) -> None:
413
+ """Consume the browser-bound challenge before starting Feishu login."""
522
414
 
523
415
  with self._lock:
524
- self._refresh_credential_locked()
416
+ self._require_admission_locked()
525
417
  now = self._now_locked()
526
418
  self._prune_locked(now)
527
419
  source = self._normalize_login_source(source_ip)
@@ -531,17 +423,51 @@ class AdminAuth:
531
423
  cookie_token=cookie_token,
532
424
  form_nonce=form_nonce,
533
425
  )
534
- candidate_digest = _token_digest(credential)
535
- supplied_digest = (
536
- _DUMMY_DIGEST if candidate_digest is None else candidate_digest
537
- )
538
- expected_digest = self._credential_digest
539
- digest_matches = hmac.compare_digest(
540
- supplied_digest,
541
- _DUMMY_DIGEST if expected_digest is None else expected_digest,
542
- )
543
- credential_valid = candidate_digest is not None and digest_matches
544
- if not (rate_allowed and nonce_valid and credential_valid):
426
+ if not (rate_allowed and nonce_valid):
427
+ if rate_allowed:
428
+ self._record_login_failure_locked(source, now)
429
+ raise LoginRejected()
430
+
431
+ def reject_login(self, source_ip: str) -> None:
432
+ """Record a failed upstream identity attempt without revealing its reason."""
433
+ with self._lock:
434
+ self._require_admission_locked()
435
+ source = self._normalize_login_source(source_ip)
436
+ now = self._now_locked()
437
+ if self._rate_allowed_locked(source, now):
438
+ self._record_login_failure_locked(source, now)
439
+ raise LoginRejected()
440
+
441
+ def precheck_for_session(
442
+ self, identity: AdminIdentity, *, source_ip: str, expected_add_revision: int,
443
+ ) -> None:
444
+ """Check the trusted login commit before persisting its user credential.
445
+
446
+ The credential writer calls this under the shared membership lock after
447
+ any per-user refresh wait. It neither allocates nor evicts a session.
448
+ """
449
+ with self._lock:
450
+ self._require_admission_locked()
451
+ source = self._normalize_login_source(source_ip)
452
+ member = self._users.get_membership(identity)
453
+ if (not self._rate_allowed_locked(source, self._now_locked())
454
+ or member is None or type(expected_add_revision) is not int
455
+ or member.add_revision != expected_add_revision):
456
+ raise LoginRejected()
457
+
458
+ def issue_session(
459
+ self, identity: AdminIdentity, *, source_ip: str, expected_add_revision: int,
460
+ ) -> IssuedSession:
461
+ """Trusted server-only handoff after Feishu identity and attempt validation."""
462
+ with self._lock:
463
+ self._require_admission_locked()
464
+ now = self._now_locked()
465
+ self._prune_locked(now)
466
+ source = self._normalize_login_source(source_ip)
467
+ member = self._users.get_membership(identity)
468
+ rate_allowed = self._rate_allowed_locked(source, now)
469
+ if (not rate_allowed or member is None or type(expected_add_revision) is not int
470
+ or member.add_revision != expected_add_revision):
545
471
  if rate_allowed:
546
472
  self._record_login_failure_locked(source, now)
547
473
  raise LoginRejected()
@@ -562,11 +488,15 @@ class AdminAuth:
562
488
  source=source,
563
489
  log_handle=log_handle,
564
490
  generation=self._generation,
491
+ identity=identity,
492
+ add_revision=member.add_revision,
565
493
  )
566
494
  return IssuedSession(
567
495
  token=token,
568
496
  log_handle=log_handle,
569
497
  generation=self._generation,
498
+ identity=identity,
499
+ add_revision=member.add_revision,
570
500
  )
571
501
 
572
502
  def authenticate(
@@ -576,7 +506,7 @@ class AdminAuth:
576
506
  """Authenticate a process-local session until it is explicitly revoked."""
577
507
 
578
508
  with self._lock:
579
- self._refresh_credential_locked()
509
+ self._require_admission_locked()
580
510
  now = self._now_locked()
581
511
  self._prune_locked(now)
582
512
  _, record = self._authenticate_session_locked(session_token)
@@ -584,7 +514,7 @@ class AdminAuth:
584
514
 
585
515
  def logout(self, session_token: object) -> bool:
586
516
  with self._lock:
587
- self._refresh_credential_locked()
517
+ self._require_admission_locked()
588
518
  now = self._now_locked()
589
519
  self._prune_locked(now)
590
520
  digest = _token_digest(session_token)
@@ -607,7 +537,7 @@ class AdminAuth:
607
537
  _require_typed_immutable_payload(target)
608
538
  _require_typed_immutable_payload(preconditions)
609
539
  with self._lock:
610
- self._refresh_credential_locked()
540
+ self._require_admission_locked()
611
541
  now = self._now_locked()
612
542
  self._prune_locked(now)
613
543
  session_digest, _ = self._authenticate_session_locked(session_token)
@@ -653,7 +583,7 @@ class AdminAuth:
653
583
  self._validate_action_kind(action_kind)
654
584
  _require_typed_immutable_payload(target)
655
585
  with self._lock:
656
- self._refresh_credential_locked()
586
+ self._require_admission_locked()
657
587
  now = self._now_locked()
658
588
  self._prune_locked(now)
659
589
  session_digest, session = self._authenticate_session_locked(
@@ -688,6 +618,8 @@ class AdminAuth:
688
618
  action_kind=record.action_kind,
689
619
  target=cast(TargetT, record.target),
690
620
  preconditions=record.preconditions,
621
+ identity=session.identity,
622
+ add_revision=session.add_revision,
691
623
  )
692
624
 
693
625
  def state_counts(self) -> AuthStateCounts:
@@ -711,32 +643,24 @@ class AdminAuth:
711
643
  with self._lock:
712
644
  self._close_admission_locked()
713
645
 
714
- def _refresh_credential_locked(self) -> bool:
646
+ def _require_admission_locked(self) -> None:
715
647
  if not self._admission_open:
716
648
  raise AdmissionClosed()
717
649
  try:
718
- snapshot = load_credential_snapshot(self._credential_path)
719
- except CredentialFileError:
650
+ self._users.has_admins
651
+ except AdminUsersUnavailable:
720
652
  self._close_admission_locked()
721
653
  raise AdmissionClosed() from None
722
- digest = self._credential_digest
723
- changed = (
724
- snapshot.identity != self._credential_identity
725
- or digest is None
726
- or not hmac.compare_digest(snapshot.digest, digest)
727
- )
728
- if not changed:
729
- return False
730
- self._credential_identity = snapshot.identity
731
- self._credential_digest = snapshot.digest
732
- self._generation += 1
733
- self._clear_bearer_state_locked()
734
- return True
654
+
655
+ def revoke_identity(self, identity: AdminIdentity) -> None:
656
+ """Revoke all browser sessions and action grants for one exact user."""
657
+ with self._lock:
658
+ for digest, record in tuple(self._sessions.items()):
659
+ if record.identity == identity:
660
+ self._drop_session_locked(digest)
735
661
 
736
662
  def _close_admission_locked(self) -> None:
737
663
  self._admission_open = False
738
- self._credential_digest = None
739
- self._credential_identity = None
740
664
  self._clear_bearer_state_locked()
741
665
 
742
666
  def _clear_bearer_state_locked(self) -> None:
@@ -832,6 +756,10 @@ class AdminAuth:
832
756
  if record is not None:
833
757
  self._drop_session_locked(digest)
834
758
  raise SessionRejected()
759
+ member = self._users.get_membership(record.identity)
760
+ if member is None or member.add_revision != record.add_revision:
761
+ self._drop_session_locked(digest)
762
+ raise SessionRejected()
835
763
  return digest, record
836
764
 
837
765
  def _authenticated_session(
@@ -841,6 +769,8 @@ class AdminAuth:
841
769
  return AuthenticatedSession(
842
770
  log_handle=record.log_handle,
843
771
  generation=record.generation,
772
+ identity=record.identity,
773
+ add_revision=record.add_revision,
844
774
  )
845
775
 
846
776
  def _drop_session_locked(self, session_digest: bytes) -> None: