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.
- {netizen_cli-0.11.0/netizen_cli.egg-info → netizen_cli-0.11.2}/PKG-INFO +3 -2
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/README.md +15 -8
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/config.example.yaml +6 -5
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/__init__.py +1 -1
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/auth.py +99 -169
- netizen_cli-0.11.2/netizen_cli/admin/credentials.py +233 -0
- netizen_cli-0.11.2/netizen_cli/admin/feishu.py +335 -0
- netizen_cli-0.11.2/netizen_cli/admin/login.py +234 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/port_config.py +2 -2
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/presentation.py +23 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/queries.py +45 -3
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/static/admin.css +110 -25
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/static/admin.js +597 -346
- netizen_cli-0.11.2/netizen_cli/admin/static/chat-picker.css +37 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/static/chat-picker.js +131 -50
- netizen_cli-0.11.2/netizen_cli/admin/static/identity.js +272 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/static/index.html +165 -89
- netizen_cli-0.11.2/netizen_cli/admin/static/login.html +29 -0
- netizen_cli-0.11.2/netizen_cli/admin/static/login.js +134 -0
- netizen_cli-0.11.2/netizen_cli/admin/users.py +378 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/web.py +327 -114
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/bindings.py +105 -16
- netizen_cli-0.11.2/netizen_cli/cards/chat_target.py +283 -0
- netizen_cli-0.11.2/netizen_cli/cards/fork.py +350 -0
- netizen_cli-0.11.2/netizen_cli/cards/pagination.py +50 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/reply.py +122 -42
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/scheduled.py +441 -189
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/reply_presenter.py +200 -50
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel_app.py +350 -86
- netizen_cli-0.11.2/netizen_cli/chat_avatars.py +114 -0
- netizen_cli-0.11.2/netizen_cli/chat_targets.py +24 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cli.py +76 -20
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cli_data.py +2 -5
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cli_services.py +0 -1
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cli_setup.py +77 -17
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cli_update.py +25 -6
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cli_update_worker.py +71 -13
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/codex_runtime.py +129 -44
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/defaults/service.py +9 -1
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/domain.py +9 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/feishu_app_onboarding.py +7 -3
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/feishu_app_permissions.py +18 -15
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/main.py +29 -33
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/management/__init__.py +2 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/management/chat_directory.py +53 -20
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/management/service.py +259 -36
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/management/updates.py +26 -0
- netizen_cli-0.11.2/netizen_cli/pagination.py +46 -0
- netizen_cli-0.11.2/netizen_cli/partial_answers.py +54 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/runtime/contracts.py +14 -6
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/schedules/service.py +43 -18
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/sdk_gap_adapter.py +15 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/service_launcher.py +0 -5
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/settings.py +2 -26
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/terminal_cleanup.py +2 -2
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/turn_activity.py +16 -2
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/turn_files.py +11 -22
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/turn_plan_observer.py +8 -2
- {netizen_cli-0.11.0 → netizen_cli-0.11.2/netizen_cli.egg-info}/PKG-INFO +3 -2
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli.egg-info/SOURCES.txt +22 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli.egg-info/requires.txt +2 -1
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/pyproject.toml +3 -2
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/skills/netizen-user-guide/references/user-guide.md +136 -48
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_bindings.py +202 -1
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cards.py +141 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_channel_app.py +4 -3
- netizen_cli-0.11.2/tests/test_chat_avatars.py +133 -0
- netizen_cli-0.11.2/tests/test_chat_target_cards.py +181 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli.py +126 -1
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli_data.py +20 -1
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli_integration.py +28 -5
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli_setup.py +106 -4
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli_update.py +163 -6
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_codex_runtime.py +369 -8
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_codex_sdk_capabilities.py +1 -1
- netizen_cli-0.11.2/tests/test_cron_chat_targets.py +461 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_deployment.py +2 -2
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_feishu_app_onboarding.py +4 -2
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_feishu_app_permissions.py +23 -7
- netizen_cli-0.11.2/tests/test_fork_cards.py +533 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_main.py +44 -25
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_migration_schema_validation.py +47 -0
- netizen_cli-0.11.2/tests/test_pagination.py +75 -0
- netizen_cli-0.11.2/tests/test_partial_answer_delivery.py +487 -0
- netizen_cli-0.11.2/tests/test_partial_answers.py +54 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_persistent_fork_bindings.py +16 -0
- netizen_cli-0.11.2/tests/test_persistent_fork_channel.py +842 -0
- netizen_cli-0.11.2/tests/test_probe_feishu_partial_answers.py +138 -0
- netizen_cli-0.11.2/tests/test_probe_persistent_fork.py +42 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_probe_scheduled_tasks.py +1 -0
- netizen_cli-0.11.2/tests/test_probe_skill_execution.py +167 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_schedule_card_retry.py +56 -1
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_schedule_cards.py +355 -10
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_schedule_scope_matrix.py +3 -1
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_schedule_service.py +109 -1
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_scheduled_channel.py +107 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_sdk_gap_adapter.py +47 -2
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_service_launcher.py +103 -8
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_session_defaults.py +24 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_settings_projects.py +12 -43
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_terminal_cleanup.py +2 -2
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_turn_activity.py +31 -1
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_turn_plan_observer.py +21 -1
- netizen_cli-0.11.0/netizen_cli/admin/static/chat-picker.css +0 -19
- netizen_cli-0.11.0/netizen_cli/cards/fork.py +0 -327
- netizen_cli-0.11.0/tests/test_fork_cards.py +0 -244
- netizen_cli-0.11.0/tests/test_persistent_fork_channel.py +0 -450
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/MANIFEST.in +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/_build.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/__main__.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/account_rate_limits.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/__init__.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/errors.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/admin/transport.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/builtin_skills.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/__init__.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/callbacks.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/controls.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/defaults.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/model_info.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/questions.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cards/usage.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/__init__.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/completion_mentions.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/input_preparation.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/messages.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/ports.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/question_inputs.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/reactions.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/channel/topics.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cli_packages.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/cli_update_restore.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/completion_mention.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/database_migrations.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/defaults/__init__.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/defaults/models.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/defaults/store.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/deployment/__init__.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/deployment/restart_worker.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/deployment/update_executor.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/deployment/update_protocol.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/error_messages.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/experience.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/feishu_links.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/git_status.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/image_inputs.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/instance.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/lark_app.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/management/blocking_io.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/management/chat_labels.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/management/coordination.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/markdown_images.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/message_content.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/message_history.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/message_preparation.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/message_projection.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/migrations/__init__.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/migrations/schema.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/migrations/v14.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/model_settings.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/package_resources.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/projects.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/prompt_projection.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/quoted_context.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/result_images.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/runtime/__init__.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/runtime/name_writes.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/runtime/thread_naming.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/schedules/__init__.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/schedules/mcp.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/schedules/models.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/schedules/scheduler.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/schedules/store.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/session_settings.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/skill_references.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/task_text.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/turn_patch_children.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli/user_questions.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli.egg-info/dependency_links.txt +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli.egg-info/entry_points.txt +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/netizen_cli.egg-info/top_level.txt +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/setup.cfg +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/skills/netizen-lark/SKILL.md +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/skills/netizen-user-guide/SKILL.md +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_account_rate_limits.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_admin_update_executor_probe.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_binding_scheduled_channel.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_channel_fixtures.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_channel_input_preparation.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_check_sdk.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli_distribution.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli_packages.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli_packaging.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_cli_services.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_codex_sdk_upgrade_0161.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_completion_mention_cards.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_completion_mentions.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_completion_topic_delivery.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_database_migrations.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_defaults_cards.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_defaults_channel.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_defaults_runtime.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_documentation_links.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_domain_experience.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_error_messages.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_file_pagination.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_git_status.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_image_inputs.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_lark_app.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_lark_sdk_contract.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_markdown_images.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_message_content.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_message_history.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_message_material_inputs.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_message_preparation.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_model_info_cards.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_model_settings.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_name_writes.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_persistent_fork_runtime.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_probe_child_files.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_probe_feishu_completion_mention.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_probe_feishu_turn_file_card.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_probe_python_sdk.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_probe_sdk_completion_race.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_probe_thread_naming.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_project_delete_channel.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_project_delete_runtime.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_project_deletion_storage.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_prompt_projection.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_question_cards.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_question_channel.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_question_target_parity.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_quoted_context.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_release_script.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_repository_hygiene.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_result_images.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_resume_channel.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_retired_deployment.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_runtime_resume_activation.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_runtime_thread_naming.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_schedule_mcp.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_schedule_run_now.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_scheduled_runtime.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_scheduler.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_scheduler_presenter.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_schedules_storage.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_session_settings.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_task_text.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_thread_naming.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_turn_files.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_turn_patch_children.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_turn_patch_statistics.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_update_executor.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_usage_card.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_usage_control.py +0 -0
- {netizen_cli-0.11.0 → netizen_cli-0.11.2}/tests/test_user_guide_skill.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
169
|
+
并在“系统维护”中查看安装信息或重启服务。发送 `/admin` 可获取当前实例的管理 URL
|
|
166
170
|
和根目录,无需创建会话。未指定端口时首次从 `8787` 起分配并保存,各实例独立监听;
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
35
|
-
#
|
|
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,7 +1,7 @@
|
|
|
1
|
-
"""In-memory authentication
|
|
1
|
+
"""In-memory browser authentication and authorization for Admin Web.
|
|
2
2
|
|
|
3
|
-
This module deliberately contains no HTTP concepts.
|
|
4
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
340
|
+
users: AdminUsers,
|
|
444
341
|
*,
|
|
445
342
|
clock: Callable[[], float] = time.monotonic,
|
|
446
343
|
limits: AuthLimits | None = None,
|
|
447
344
|
) -> None:
|
|
448
|
-
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
-
|
|
520
|
-
|
|
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.
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
)
|
|
543
|
-
|
|
544
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
646
|
+
def _require_admission_locked(self) -> None:
|
|
715
647
|
if not self._admission_open:
|
|
716
648
|
raise AdmissionClosed()
|
|
717
649
|
try:
|
|
718
|
-
|
|
719
|
-
except
|
|
650
|
+
self._users.has_admins
|
|
651
|
+
except AdminUsersUnavailable:
|
|
720
652
|
self._close_admission_locked()
|
|
721
653
|
raise AdmissionClosed() from None
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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:
|