devops-bot-sdk 1.6.34__tar.gz → 1.6.37__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.
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/SOURCES.txt +5 -2
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/entry_points.txt +1 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/pyproject.toml +1 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/cli.py +31 -1
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/client.py +65 -38
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/config.py +9 -1
- devops_bot_sdk-1.6.37/sdk/firewall.py +310 -0
- devops_bot_sdk-1.6.37/sdk/github_ingest.py +573 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/run_auto.py +17 -24
- devops_bot_sdk-1.6.37/sdk/swagger.py +356 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/api.py +0 -27
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/ip_allowlist.py +43 -16
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/models.py +0 -13
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/server.py +125 -17
- devops_bot_sdk-1.6.37/sdk/vps_health.py +264 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_github_ingest.py +8 -4
- devops_bot_sdk-1.6.37/tests/test_swagger_command.py +260 -0
- devops_bot_sdk-1.6.37/tests/test_ui_gate.py +281 -0
- devops_bot_sdk-1.6.37/tests/test_ui_lifespan.py +89 -0
- devops_bot_sdk-1.6.34/sdk/github_ingest.py +0 -161
- devops_bot_sdk-1.6.34/sdk/ui/static/login.html +0 -111
- devops_bot_sdk-1.6.34/sdk/vps_health.py +0 -111
- devops_bot_sdk-1.6.34/tests/test_otp_login.py +0 -178
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/README.md +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/browser_profile.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/estimator.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/journal.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/prd_progress.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/providers.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/resume.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/router.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/self_report.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/self_report_local.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/task_types.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/tiers/__init__.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/tiers/mini_agent.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/url_access.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/write_hook.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_browser_profile.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_cheap_tier.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_estimator.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_journal.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_local_exec_provider.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_prd_artifact.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_providers.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_resume.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_router.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_self_report.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_tier_flag_parity.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_url_access.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_vps_health.py +0 -0
- {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_write_hook.py +0 -0
|
@@ -16,6 +16,7 @@ sdk/crucial.py
|
|
|
16
16
|
sdk/departments.py
|
|
17
17
|
sdk/estimator.py
|
|
18
18
|
sdk/exceptions.py
|
|
19
|
+
sdk/firewall.py
|
|
19
20
|
sdk/formats.py
|
|
20
21
|
sdk/git_ops.py
|
|
21
22
|
sdk/github_ingest.py
|
|
@@ -38,6 +39,7 @@ sdk/scheduler.py
|
|
|
38
39
|
sdk/self_report.py
|
|
39
40
|
sdk/self_report_local.py
|
|
40
41
|
sdk/sse.py
|
|
42
|
+
sdk/swagger.py
|
|
41
43
|
sdk/task_file.py
|
|
42
44
|
sdk/task_types.py
|
|
43
45
|
sdk/test.py
|
|
@@ -112,7 +114,6 @@ sdk/ui/static/chat.html
|
|
|
112
114
|
sdk/ui/static/custom-agents.html
|
|
113
115
|
sdk/ui/static/default-agents.html
|
|
114
116
|
sdk/ui/static/index.html
|
|
115
|
-
sdk/ui/static/login.html
|
|
116
117
|
sdk/ui/static/report.html
|
|
117
118
|
sdk/ui/static/runs.html
|
|
118
119
|
sdk/ui/static/schedules.html
|
|
@@ -152,7 +153,6 @@ tests/test_health.py
|
|
|
152
153
|
tests/test_intake_plan_review.py
|
|
153
154
|
tests/test_journal.py
|
|
154
155
|
tests/test_local_exec_provider.py
|
|
155
|
-
tests/test_otp_login.py
|
|
156
156
|
tests/test_phase_timer.py
|
|
157
157
|
tests/test_plan_new_command.py
|
|
158
158
|
tests/test_plan_run_activity.py
|
|
@@ -168,12 +168,15 @@ tests/test_schedule_api.py
|
|
|
168
168
|
tests/test_scheduler.py
|
|
169
169
|
tests/test_self_report.py
|
|
170
170
|
tests/test_service_token_warning.py
|
|
171
|
+
tests/test_swagger_command.py
|
|
171
172
|
tests/test_task_file_and_plan_run.py
|
|
172
173
|
tests/test_task_file_format_doc.py
|
|
173
174
|
tests/test_ticket_fields.py
|
|
174
175
|
tests/test_ticket_run_resolve_by_id.py
|
|
175
176
|
tests/test_ticket_speed_fixes.py
|
|
176
177
|
tests/test_tier_flag_parity.py
|
|
178
|
+
tests/test_ui_gate.py
|
|
179
|
+
tests/test_ui_lifespan.py
|
|
177
180
|
tests/test_update_timer.py
|
|
178
181
|
tests/test_updater_extras.py
|
|
179
182
|
tests/test_url_access.py
|
|
@@ -54,6 +54,7 @@ agentos = "sdk.cli:main" # unified CLI: configure/agent/integ
|
|
|
54
54
|
agentos-auto = "sdk.run_auto:main" # run orchestrate-auto LOCALLY (co_ token from config)
|
|
55
55
|
agentos-agentd = "sdk.agentd:main" # long-running custom-agent daemon
|
|
56
56
|
agentos-ui = "sdk.ui.server:main" # local web control panel (needs the [ui] extra)
|
|
57
|
+
agentos-swagger = "sdk.swagger:main" # where /docs is + can you reach it (= `agentos swagger`)
|
|
57
58
|
|
|
58
59
|
[project.urls]
|
|
59
60
|
Homepage = "https://agentos.io"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
|
|
2
2
|
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.37
|
|
4
4
|
|
|
5
5
|
Public surface:
|
|
6
6
|
BackendClient.from_config() — create client from ~/.agentos/config.toml
|
|
@@ -34,7 +34,7 @@ Rules:
|
|
|
34
34
|
- All data egress through submit_webhook only
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
-
__version__ = "1.6.
|
|
37
|
+
__version__ = "1.6.37" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
|
|
38
38
|
__author__ = "AgentOS" # sdk.client.SDK_VERSION and the UI server version all read this.
|
|
39
39
|
|
|
40
40
|
from sdk.client import BackendClient
|
|
@@ -58,6 +58,26 @@ Command surface:
|
|
|
58
58
|
agentos integration rm <name>
|
|
59
59
|
Remove a registered integration.
|
|
60
60
|
|
|
61
|
+
agentos firewall [--apply] [--revoke IP] [--port N]
|
|
62
|
+
Print the host-firewall rules that open the agentos-ui port to every IP in
|
|
63
|
+
the application allowlist (`sdk.config.get_allowed_ips`), for THIS OS — ufw
|
|
64
|
+
on Linux, `netsh advfirewall` on Windows, a pf note on macOS. Prints by
|
|
65
|
+
default; `--apply` executes them and needs root / Administrator. The host
|
|
66
|
+
firewall is a separate layer the allowlist cannot reach, and the two drifting
|
|
67
|
+
apart is what silently blinded every VPS when the backend's egress IP moved.
|
|
68
|
+
See sdk.firewall.
|
|
69
|
+
|
|
70
|
+
agentos swagger [--open] [--json [PATH]] [--port N]
|
|
71
|
+
Where the panel's Swagger UI is and whether it can actually be reached:
|
|
72
|
+
the URL derived from the bind address the service was installed with,
|
|
73
|
+
whether a co_ token is configured (masked), how many operations the spec
|
|
74
|
+
has, and a ufw/netsh diff against sdk.config.get_allowed_ips when the HOST
|
|
75
|
+
firewall would drop the port — a layer the SDK's own allowlist cannot see.
|
|
76
|
+
`--open` launches a browser; `--json` writes openapi.json from
|
|
77
|
+
create_app().openapi() with no server running (`--json -` pipes it).
|
|
78
|
+
Also flags routes registered twice, since the second copy is dead code.
|
|
79
|
+
Needs the [ui] extra for the route count and --json. See sdk.swagger.
|
|
80
|
+
|
|
61
81
|
agentos report submit [--date YYYY-MM-DD] (--text "…" | --file PATH)
|
|
62
82
|
[--project NAME] [--proof PATH|URL]… [--no-wait]
|
|
63
83
|
Report work that never reached Jira or GitHub, attach proofs (local
|
|
@@ -139,6 +159,14 @@ def main() -> None:
|
|
|
139
159
|
_github_command(rest)
|
|
140
160
|
return
|
|
141
161
|
|
|
162
|
+
if noun == "firewall":
|
|
163
|
+
from sdk import firewall
|
|
164
|
+
sys.exit(firewall.cli(rest))
|
|
165
|
+
|
|
166
|
+
if noun == "swagger":
|
|
167
|
+
from sdk import swagger
|
|
168
|
+
sys.exit(swagger.cli(rest))
|
|
169
|
+
|
|
142
170
|
if noun == "report":
|
|
143
171
|
_report_command(rest)
|
|
144
172
|
return
|
|
@@ -152,7 +180,7 @@ def main() -> None:
|
|
|
152
180
|
|
|
153
181
|
|
|
154
182
|
def _print_usage() -> None:
|
|
155
|
-
print("usage: agentos <configure|chat|agent|integration|default-agents|agentd|github|report|browser> ...")
|
|
183
|
+
print("usage: agentos <configure|chat|agent|integration|default-agents|agentd|github|report|browser|firewall|swagger> ...")
|
|
156
184
|
print()
|
|
157
185
|
print(" agentos configure [--rotate]")
|
|
158
186
|
print(" agentos chat [--product-id ID] [--pv-id ID] [--no-autorun] # requirements → ticket")
|
|
@@ -164,6 +192,8 @@ def _print_usage() -> None:
|
|
|
164
192
|
print(" agentos github <ingest [--days N]|status> # PR ingestion; --days N backfills N days")
|
|
165
193
|
print(" agentos report <submit|list|show ID> # report work Jira/GitHub never saw")
|
|
166
194
|
print(" agentos browser <login URL|status|logout> # sign in once; private links become readable")
|
|
195
|
+
print(" agentos firewall [--apply] [--revoke IP] [--port N] # open the UI port to the allowlisted IPs")
|
|
196
|
+
print(" agentos swagger [--open] [--json [PATH]] # where /docs is, and can you reach it")
|
|
167
197
|
|
|
168
198
|
|
|
169
199
|
# ── agent ────────────────────────────────────────────────────────────────
|
|
@@ -45,6 +45,13 @@ logger = logging.getLogger(__name__)
|
|
|
45
45
|
# agents without a lookup on every run.
|
|
46
46
|
_AGENT_ID_CACHE: dict[str, str] = {}
|
|
47
47
|
|
|
48
|
+
# GitHub access token, keyed by the co_ token that fetched it → (token, expires_at
|
|
49
|
+
# monotonic). Keyed per credential so two clients on one box never share a token.
|
|
50
|
+
# Without this the token is re-fetched on EVERY call, which is what
|
|
51
|
+
# tests/test_ticket_speed_fixes.py::TestGithubTokenCache guards against.
|
|
52
|
+
_GITHUB_TOKEN_CACHE: dict[str, tuple[str, float]] = {}
|
|
53
|
+
_GITHUB_TOKEN_TTL_S = float(os.getenv("AGENTOS_GITHUB_TOKEN_TTL_S", "") or 1800.0)
|
|
54
|
+
|
|
48
55
|
# GitHub access token, keyed by the co_ token that fetched it → (token, expires_at
|
|
49
56
|
# monotonic). Keyed per credential so two clients on one box never share a token.
|
|
50
57
|
_GITHUB_TOKEN_CACHE: dict[str, tuple[str, float]] = {}
|
|
@@ -952,51 +959,71 @@ class BackendClient:
|
|
|
952
959
|
co_ token. Best-effort: returns None on any failure so a run without a
|
|
953
960
|
connected GitHub account still proceeds (clone/PR steps just won't auth).
|
|
954
961
|
"""
|
|
962
|
+
import time as _time
|
|
963
|
+
|
|
964
|
+
now = _time.monotonic()
|
|
965
|
+
cached = _GITHUB_TOKEN_CACHE.get(self._token)
|
|
966
|
+
if cached and now < cached[1]:
|
|
967
|
+
return cached[0]
|
|
955
968
|
record = await self.github_token_record()
|
|
956
|
-
|
|
969
|
+
token = (record or {}).get("accessToken") or None
|
|
970
|
+
if token:
|
|
971
|
+
_GITHUB_TOKEN_CACHE[self._token] = (token, now + _GITHUB_TOKEN_TTL_S)
|
|
972
|
+
return token
|
|
957
973
|
|
|
958
|
-
async def ingest_github_prs(self, since: str | None = None) -> dict:
|
|
974
|
+
async def ingest_github_prs(self, since: str | None = None) -> dict | None:
|
|
959
975
|
"""POST {AI}/api/v1/github/prs — trigger this user's daily PR ingestion.
|
|
960
976
|
|
|
961
|
-
The
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
977
|
+
The daily trigger. The work runs on the AI backend (LLM summarise, the
|
|
978
|
+
downstream daily-summary forward, the Qdrant upsert); the SDK only decides
|
|
979
|
+
WHEN, because only this machine knows its own catch-up watermark.
|
|
980
|
+
|
|
981
|
+
The backend resolves the caller's GitHub link server-side from the co_ token
|
|
982
|
+
in ``Authorization`` and OVERWRITES whatever the body sends
|
|
983
|
+
(``docs/GITHUB_SCHEDULING_SDK_MIGRATION.md`` §7/§9 — that endpoint used to
|
|
984
|
+
take a GitHub access token in an unauthenticated body). So this deliberately
|
|
985
|
+
sends NO credentials: a client that still supplied them would be shipping a
|
|
986
|
+
secret the server ignores.
|
|
987
|
+
|
|
988
|
+
``since`` is an ISO date (``YYYY-MM-DD``) → the backend's ``date_from``.
|
|
989
|
+
Omitted means "today", which is what a never-missed run wants; the cron sends
|
|
990
|
+
the watermark instead so a machine that was off does not lose days.
|
|
991
|
+
|
|
992
|
+
Returns the response JSON (``{status, job_id, status_url, ...}``) or **None**
|
|
993
|
+
on any failure — never raises into the cron. WHY it failed lands in
|
|
994
|
+
``self.last_ingest_error`` (``{status, body, retryable}``), which the
|
|
995
|
+
scheduler needs to tell a busy backend (retry in 30 min) from a rejected
|
|
996
|
+
request (wait for the next slot).
|
|
970
997
|
"""
|
|
971
|
-
|
|
998
|
+
payload = [{
|
|
972
999
|
"userId": self._user_id_from_token(),
|
|
973
|
-
"
|
|
974
|
-
"githubAccessToken": "",
|
|
975
|
-
"apiTokenHash": self._token,
|
|
976
|
-
"date_from": since,
|
|
1000
|
+
"apiTokenHash": self._token, # downstream daily-summary Authorization
|
|
977
1001
|
}]
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1002
|
+
if since:
|
|
1003
|
+
payload[0]["date_from"] = since
|
|
1004
|
+
self.last_ingest_error = {}
|
|
1005
|
+
try:
|
|
1006
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
1007
|
+
resp = await client.post(
|
|
1008
|
+
self._url("/api/v1/github/prs"), json=payload, headers=self._headers,
|
|
1009
|
+
)
|
|
1010
|
+
if resp.status_code >= 400:
|
|
1011
|
+
body = resp.text[:500]
|
|
1012
|
+
logger.warning("client.ingest_github_prs failed status=%s body=%s",
|
|
1013
|
+
resp.status_code, body[:200])
|
|
1014
|
+
self.last_ingest_error = {
|
|
1015
|
+
"status": resp.status_code,
|
|
1016
|
+
"body": body,
|
|
1017
|
+
"retryable": is_retryable_backend_error(resp.status_code, body),
|
|
1018
|
+
}
|
|
1019
|
+
return None
|
|
1020
|
+
return resp.json()
|
|
1021
|
+
except Exception as exc: # noqa: BLE001 — a cron must not die on a blip
|
|
1022
|
+
logger.warning("client.ingest_github_prs error err=%s", exc)
|
|
1023
|
+
# A transport failure (timeout, reset, DNS blip) is as retryable as an
|
|
1024
|
+
# exhausted pool — the backend may simply be busy.
|
|
1025
|
+
self.last_ingest_error = {"status": 0, "body": str(exc)[:500], "retryable": True}
|
|
1026
|
+
return None
|
|
1000
1027
|
|
|
1001
1028
|
async def move_to_backlog(self, task_id: str) -> bool:
|
|
1002
1029
|
"""PUT {BE}/tasks-ml/{id} status=backlog (best-effort).
|
|
@@ -192,7 +192,15 @@ def _dotenv_value(key: str) -> str:
|
|
|
192
192
|
# backend can reach EVERY VPS's /api/ui/* out of the box (no per-VPS .env needed);
|
|
193
193
|
# always allowed, treated as permanent (not removable via the API). Extend/override
|
|
194
194
|
# with AGENTOS_UI_ALLOWED_IPS in .env or the process env.
|
|
195
|
-
|
|
195
|
+
#
|
|
196
|
+
# A LIST, not one value, because the backend's egress address is not stable: when it
|
|
197
|
+
# moved to 20.127.87.169 every VPS silently stopped answering `GET /api/v1/vps/health`
|
|
198
|
+
# (30s hang, then 502 — the SYNs were dropped at the host firewall, observed
|
|
199
|
+
# 2026-09-03). Both addresses stay listed rather than swapping, so a rollback or a
|
|
200
|
+
# blue/green pair does not re-break it. Note the host firewall is a SEPARATE layer this
|
|
201
|
+
# list cannot reach: `sudo ufw allow from <ip> to any port 8765 proto tcp` is still
|
|
202
|
+
# required per VPS. See docs/VPS_HEALTH_AND_GITHUB_INGEST_DIAGNOSIS.md #5.
|
|
203
|
+
_DEFAULT_ALLOWED_IPS: list[str] = ["20.12.201.148", "20.127.87.169"]
|
|
196
204
|
|
|
197
205
|
|
|
198
206
|
def _parse_token_list(raw: str) -> list[str]:
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Host-firewall rules for the ``agentos-ui`` port — generate, show, apply.
|
|
2
|
+
|
|
3
|
+
The SDK has **two** independent gates in front of `/api/ui/*`, and only one of
|
|
4
|
+
them is code:
|
|
5
|
+
|
|
6
|
+
1. `sdk.config.get_allowed_ips()` + `IPAllowlistMiddleware` — application level,
|
|
7
|
+
shipped in the wheel, changed by editing `.env` / `config.toml`.
|
|
8
|
+
2. The **host firewall** — OS level, outside the process entirely.
|
|
9
|
+
|
|
10
|
+
Nothing in the SDK could reach (2), which is how the backend's egress move to
|
|
11
|
+
`20.127.87.169` silently blinded every VPS: the allowlist said yes, the kernel
|
|
12
|
+
dropped the SYN before the server ever saw it (`docs/VPS_HEALTH_AND_GITHUB_INGEST
|
|
13
|
+
_DIAGNOSIS.md` #5). This module closes that gap by deriving the firewall rules
|
|
14
|
+
from the *same* list the middleware enforces, so the two layers cannot drift.
|
|
15
|
+
|
|
16
|
+
agentos firewall # show the rules for this OS (safe, prints only)
|
|
17
|
+
agentos firewall --apply # run them (needs root / Administrator)
|
|
18
|
+
agentos firewall --revoke IP # print the matching delete rule
|
|
19
|
+
|
|
20
|
+
Applying is deliberately opt-in: editing a live firewall is exactly the operation
|
|
21
|
+
that can lock an operator out of their own box, so the default is to print and let
|
|
22
|
+
a human read it first.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import platform
|
|
28
|
+
import shutil
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
_RULE_TAG = "AgentOS UI" # stable prefix so rules are findable and removable
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def ui_port() -> int:
|
|
36
|
+
"""The port `agentos-ui` binds — same resolution the server uses."""
|
|
37
|
+
try:
|
|
38
|
+
return int(os.environ.get("AGENTOS_UI_PORT", "8765"))
|
|
39
|
+
except ValueError:
|
|
40
|
+
return 8765
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _system() -> str:
|
|
44
|
+
return platform.system().lower()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def allow_rules(ips: list[str], port: int, system: str | None = None) -> list[str]:
|
|
48
|
+
"""The commands that open ``port`` to each of ``ips`` — one per IP.
|
|
49
|
+
|
|
50
|
+
Linux uses ufw. Windows uses `netsh advfirewall`, whose rule model differs in
|
|
51
|
+
two ways worth naming: the rule is identified by `name=`, not by its contents,
|
|
52
|
+
so a stable name is what makes it idempotent-ish and removable; and `remoteip=`
|
|
53
|
+
accepts a comma-separated list, so all the IPs *could* be one rule — kept as
|
|
54
|
+
one-rule-per-IP anyway so a single address can be revoked without rewriting
|
|
55
|
+
the others.
|
|
56
|
+
"""
|
|
57
|
+
sysname = (system or _system()).lower()
|
|
58
|
+
out: list[str] = []
|
|
59
|
+
|
|
60
|
+
if sysname == "linux":
|
|
61
|
+
for ip in ips:
|
|
62
|
+
# `comment` must come last; ufw >= 0.35. Harmless to re-run — ufw
|
|
63
|
+
# reports "Skipping adding existing rule".
|
|
64
|
+
out.append(
|
|
65
|
+
f"sudo ufw allow from {ip} to any port {port} proto tcp "
|
|
66
|
+
f"comment '{_RULE_TAG}'"
|
|
67
|
+
)
|
|
68
|
+
elif sysname == "windows":
|
|
69
|
+
for ip in ips:
|
|
70
|
+
out.append(
|
|
71
|
+
f'netsh advfirewall firewall add rule name="{_RULE_TAG} {port} {ip}" '
|
|
72
|
+
f"dir=in action=allow protocol=TCP localport={port} remoteip={ip}"
|
|
73
|
+
)
|
|
74
|
+
elif sysname == "darwin":
|
|
75
|
+
# macOS ships pf, which has no per-rule CLI equivalent — rules live in a
|
|
76
|
+
# file and the anchor is reloaded. Emitted as a here-doc + reload rather
|
|
77
|
+
# than pretending a one-liner exists.
|
|
78
|
+
joined = ", ".join(ips) or "none"
|
|
79
|
+
out.append(
|
|
80
|
+
f"# macOS (pf) — append to /etc/pf.anchors/agentos then reload:\n"
|
|
81
|
+
f"# pass in proto tcp from {{ {joined} }} to any port {port}\n"
|
|
82
|
+
f"sudo pfctl -f /etc/pf.conf && sudo pfctl -E"
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
out.append(f"# unsupported platform {sysname!r} — open TCP {port} to: "
|
|
86
|
+
f"{', '.join(ips) or 'none'}")
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def revoke_rule(ip: str, port: int, system: str | None = None) -> str:
|
|
91
|
+
"""The command that removes the rule for one ``ip``."""
|
|
92
|
+
sysname = (system or _system()).lower()
|
|
93
|
+
if sysname == "linux":
|
|
94
|
+
return f"sudo ufw delete allow from {ip} to any port {port} proto tcp"
|
|
95
|
+
if sysname == "windows":
|
|
96
|
+
return (f'netsh advfirewall firewall delete rule '
|
|
97
|
+
f'name="{_RULE_TAG} {port} {ip}" remoteip={ip}')
|
|
98
|
+
if sysname == "darwin":
|
|
99
|
+
return f"# macOS: remove the matching line from /etc/pf.anchors/agentos, then: sudo pfctl -f /etc/pf.conf"
|
|
100
|
+
return f"# unsupported platform {sysname!r}"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _tool_present() -> tuple[bool, str]:
|
|
104
|
+
"""Is the firewall tool for this OS actually installed?"""
|
|
105
|
+
sysname = _system()
|
|
106
|
+
tool = {"linux": "ufw", "windows": "netsh", "darwin": "pfctl"}.get(sysname, "")
|
|
107
|
+
if not tool:
|
|
108
|
+
return False, f"no known firewall tool for {sysname}"
|
|
109
|
+
if shutil.which(tool):
|
|
110
|
+
return True, tool
|
|
111
|
+
return False, f"{tool} not found on PATH"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _run(cmd: list[str], timeout: float = 10.0) -> str | None:
|
|
115
|
+
"""Run a read-only query and return stdout, or None if it could not be run."""
|
|
116
|
+
try:
|
|
117
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
118
|
+
except Exception: # noqa: BLE001 — tool missing, no permission, hung
|
|
119
|
+
return None
|
|
120
|
+
return proc.stdout if proc.returncode == 0 else None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _parse_ufw(text: str, port: int) -> set[str]:
|
|
124
|
+
"""Sources ufw currently allows to ``port`` (IPv4 rules only).
|
|
125
|
+
|
|
126
|
+
Parses the human table, because `ufw status` has no machine format. A row is
|
|
127
|
+
``<to> <action> <from>`` and the interesting ones look like::
|
|
128
|
+
|
|
129
|
+
8765/tcp ALLOW 20.12.201.148 # app port - requested IP
|
|
130
|
+
8765/tcp ALLOW Anywhere
|
|
131
|
+
|
|
132
|
+
``(v6)`` rows are skipped so the result can be diffed against an IPv4
|
|
133
|
+
allowlist without spurious "missing" entries.
|
|
134
|
+
"""
|
|
135
|
+
out: set[str] = set()
|
|
136
|
+
for line in text.splitlines():
|
|
137
|
+
line = line.split("#", 1)[0].strip()
|
|
138
|
+
if not line or "(v6)" in line:
|
|
139
|
+
continue
|
|
140
|
+
parts = line.split()
|
|
141
|
+
if len(parts) < 3 or parts[1].upper() != "ALLOW":
|
|
142
|
+
continue
|
|
143
|
+
to = parts[0]
|
|
144
|
+
if to != str(port) and not to.startswith(f"{port}/"):
|
|
145
|
+
continue
|
|
146
|
+
out.add(" ".join(parts[2:]).strip())
|
|
147
|
+
return out
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _parse_netsh(text: str, port: int) -> set[str]:
|
|
151
|
+
"""Sources netsh currently allows to ``port`` (inbound allow rules only).
|
|
152
|
+
|
|
153
|
+
`show rule` emits blank-line-separated blocks of ``Key: Value``; a rule matters
|
|
154
|
+
only when it is inbound, allowing, and its LocalPort covers ``port``.
|
|
155
|
+
"""
|
|
156
|
+
out: set[str] = set()
|
|
157
|
+
for block in text.split("\n\n"):
|
|
158
|
+
fields: dict[str, str] = {}
|
|
159
|
+
for line in block.splitlines():
|
|
160
|
+
if ":" in line:
|
|
161
|
+
k, _, v = line.partition(":")
|
|
162
|
+
fields[k.strip().lower()] = v.strip()
|
|
163
|
+
if not fields:
|
|
164
|
+
continue
|
|
165
|
+
if fields.get("enabled", "").lower() not in ("yes", ""):
|
|
166
|
+
continue
|
|
167
|
+
if fields.get("direction", "").lower() not in ("in", "inbound", ""):
|
|
168
|
+
continue
|
|
169
|
+
if fields.get("action", "").lower() not in ("allow", ""):
|
|
170
|
+
continue
|
|
171
|
+
ports = {p.strip() for p in fields.get("localport", "").split(",")}
|
|
172
|
+
if str(port) not in ports and "any" not in {p.lower() for p in ports}:
|
|
173
|
+
continue
|
|
174
|
+
for ip in fields.get("remoteip", "").split(","):
|
|
175
|
+
ip = ip.strip()
|
|
176
|
+
if ip:
|
|
177
|
+
out.add("Anywhere" if ip.lower() in ("any", "*") else ip)
|
|
178
|
+
return out
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def port_sources(port: int, system: str | None = None) -> set[str] | None:
|
|
182
|
+
"""Which sources the HOST firewall currently allows to ``port``.
|
|
183
|
+
|
|
184
|
+
``{"Anywhere"}`` means open to the world. **``None`` means the state could not
|
|
185
|
+
be read** — no tool, or not privileged — and a caller must then say "unknown",
|
|
186
|
+
never "closed": reporting a closed port that is actually open is the more
|
|
187
|
+
dangerous error of the two.
|
|
188
|
+
|
|
189
|
+
Read-only and never raises. This exists because the SDK's own allowlist
|
|
190
|
+
(`sdk.config.get_allowed_ips`) and the host firewall are separate layers, and
|
|
191
|
+
the interesting question is where they DISAGREE — an IP the app permits but the
|
|
192
|
+
kernel drops looks exactly like a broken backend.
|
|
193
|
+
"""
|
|
194
|
+
sysname = (system or _system()).lower()
|
|
195
|
+
if sysname == "linux":
|
|
196
|
+
text = _run(["ufw", "status"])
|
|
197
|
+
if text is None:
|
|
198
|
+
return None
|
|
199
|
+
got = _parse_ufw(text, port)
|
|
200
|
+
return {"Anywhere"} if "Anywhere" in got else got
|
|
201
|
+
if sysname == "windows":
|
|
202
|
+
text = _run(["netsh", "advfirewall", "firewall", "show", "rule",
|
|
203
|
+
"name=all", "dir=in"], timeout=25.0)
|
|
204
|
+
if text is None:
|
|
205
|
+
return None
|
|
206
|
+
got = _parse_netsh(text, port)
|
|
207
|
+
return {"Anywhere"} if "Anywhere" in got else got
|
|
208
|
+
return None # macOS pf and anything else: not readable this way
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _is_privileged() -> bool:
|
|
212
|
+
if _system() == "windows":
|
|
213
|
+
try:
|
|
214
|
+
import ctypes
|
|
215
|
+
return bool(ctypes.windll.shell32.IsUserAnAdmin())
|
|
216
|
+
except Exception: # noqa: BLE001 — not Windows, or no ctypes
|
|
217
|
+
return False
|
|
218
|
+
return hasattr(os, "geteuid") and os.geteuid() == 0
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def cli(argv: list[str]) -> int:
|
|
222
|
+
"""``agentos firewall [--apply] [--revoke IP] [--port N]``."""
|
|
223
|
+
from sdk.config import get_allowed_ips
|
|
224
|
+
|
|
225
|
+
port = ui_port()
|
|
226
|
+
if "--port" in argv:
|
|
227
|
+
try:
|
|
228
|
+
port = int(argv[argv.index("--port") + 1])
|
|
229
|
+
except (IndexError, ValueError):
|
|
230
|
+
print("error: --port needs a number")
|
|
231
|
+
return 2
|
|
232
|
+
|
|
233
|
+
if "--revoke" in argv:
|
|
234
|
+
try:
|
|
235
|
+
ip = argv[argv.index("--revoke") + 1]
|
|
236
|
+
except IndexError:
|
|
237
|
+
print("error: --revoke needs an IP")
|
|
238
|
+
return 2
|
|
239
|
+
print(revoke_rule(ip, port))
|
|
240
|
+
print("\nAlso remove it from the allowlist, or the next restart re-permits it\n"
|
|
241
|
+
"at the application layer: AGENTOS_UI_ALLOWED_IPS in ~/.agentos/.env,\n"
|
|
242
|
+
"or `allowed_ips` in ~/.agentos/config.toml. IPs baked into\n"
|
|
243
|
+
"sdk.config._DEFAULT_ALLOWED_IPS cannot be removed that way.")
|
|
244
|
+
return 0
|
|
245
|
+
|
|
246
|
+
ips = get_allowed_ips()
|
|
247
|
+
sysname = _system()
|
|
248
|
+
print(f"Platform : {platform.system()} ({sysname})")
|
|
249
|
+
print(f"UI port : {port}")
|
|
250
|
+
print(f"Allowed : {', '.join(ips) if ips else '(none — loopback only)'}")
|
|
251
|
+
|
|
252
|
+
if not ips:
|
|
253
|
+
print("\nNothing to open. Loopback always reaches the server; add the backend's\n"
|
|
254
|
+
"egress IP to AGENTOS_UI_ALLOWED_IPS first.")
|
|
255
|
+
return 0
|
|
256
|
+
|
|
257
|
+
rules = allow_rules(ips, port, sysname)
|
|
258
|
+
print("\nFirewall rules for this host:\n")
|
|
259
|
+
for r in rules:
|
|
260
|
+
print(f" {r}")
|
|
261
|
+
|
|
262
|
+
if "--apply" not in argv:
|
|
263
|
+
ok, tool = _tool_present()
|
|
264
|
+
print(f"\n(dry run — nothing changed. Firewall tool: {tool})")
|
|
265
|
+
print("Re-run with --apply to execute, or copy the lines above.")
|
|
266
|
+
if sysname == "linux":
|
|
267
|
+
print("Verify after applying: sudo ufw status")
|
|
268
|
+
elif sysname == "windows":
|
|
269
|
+
print(f'Verify after applying: netsh advfirewall firewall show rule name="{_RULE_TAG} {port} {ips[0]}"')
|
|
270
|
+
return 0
|
|
271
|
+
|
|
272
|
+
if sysname == "darwin":
|
|
273
|
+
print("\n--apply is not supported on macOS: pf rules live in a file, so applying\n"
|
|
274
|
+
"them means editing /etc/pf.anchors/agentos by hand. Follow the note above.")
|
|
275
|
+
return 1
|
|
276
|
+
|
|
277
|
+
ok, tool = _tool_present()
|
|
278
|
+
if not ok:
|
|
279
|
+
print(f"\nerror: {tool}")
|
|
280
|
+
return 1
|
|
281
|
+
if not _is_privileged():
|
|
282
|
+
who = "Administrator" if sysname == "windows" else "root (sudo)"
|
|
283
|
+
print(f"\nerror: --apply needs {who}.")
|
|
284
|
+
return 1
|
|
285
|
+
|
|
286
|
+
failed = 0
|
|
287
|
+
print()
|
|
288
|
+
for r in rules:
|
|
289
|
+
# The printed form carries `sudo` for humans; we are already privileged.
|
|
290
|
+
cmd = r[len("sudo "):] if r.startswith("sudo ") else r
|
|
291
|
+
try:
|
|
292
|
+
proc = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
|
|
293
|
+
except Exception as exc: # noqa: BLE001 — never leave the firewall half-known
|
|
294
|
+
print(f" ✗ {cmd}\n {exc}")
|
|
295
|
+
failed += 1
|
|
296
|
+
continue
|
|
297
|
+
tail = (proc.stdout or proc.stderr or "").strip().splitlines()
|
|
298
|
+
note = tail[-1][:100] if tail else ""
|
|
299
|
+
if proc.returncode == 0:
|
|
300
|
+
print(f" ✓ {cmd}\n {note}" if note else f" ✓ {cmd}")
|
|
301
|
+
else:
|
|
302
|
+
print(f" ✗ {cmd} (exit {proc.returncode})\n {note}")
|
|
303
|
+
failed += 1
|
|
304
|
+
|
|
305
|
+
print(f"\n{len(rules) - failed}/{len(rules)} applied.")
|
|
306
|
+
return 1 if failed else 0
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
if __name__ == "__main__": # pragma: no cover
|
|
310
|
+
sys.exit(cli(sys.argv[1:]))
|