devops-bot-sdk 1.7.43__tar.gz → 1.7.45__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.7.43 → devops_bot_sdk-1.7.45}/PKG-INFO +1 -1
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/firewall.py +178 -39
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/help_data/areas.json +4 -4
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/help_data/releases.json +10 -0
- devops_bot_sdk-1.7.45/sdk/help_data/snapshot.json +17 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/api.py +27 -2
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/ip_allowlist.py +182 -51
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/security.py +22 -2
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/server.py +15 -1
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/updater.py +13 -5
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_firewall_open_on_update.py +131 -15
- devops_bot_sdk-1.7.45/tests/test_ui_bruteforce_guard.py +170 -0
- devops_bot_sdk-1.7.43/sdk/help_data/snapshot.json +0 -17
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/README.md +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/pyproject.toml +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/activity/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/activity/classifier.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/activity/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/activity/collectors/common.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/activity/collectors/linux_auditd.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/activity/collectors/macos_eslogger.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/activity/collectors/windows_eventlog.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/activity/models.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/activity/os_setup.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/activity/recorder.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/activity/redact.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/activity/settings.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/activity/shipper.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/activity/spool.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agent_sharing.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/browser_profile.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/claude_usage.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/client.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/config.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/custom_agents.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/deferred_tickets.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/estimator.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/fallback_handoff.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/faqs.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/github_fetch.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/help_actions.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/help_data/commands.json +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/help_data/help.json +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/help_data/pages.json +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/helpdesk.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/helpdesk_kb.md +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/instructions_template.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/journal.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/log_colors.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/memory_guard.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/model_policy.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/prd_progress.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/providers.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/resume.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/router.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/schedule_tickets.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/schedule_timeline.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/scope_plan.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/self_report.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/self_report_local.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/swagger.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/task_types.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/test.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/test_env.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ticket_board.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ticket_bulk.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ticket_plan.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ticket_pools.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/tickets.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/tiers/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/tiers/mini_agent.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/static/faqs.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/static/help.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/ui/swagger.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/url_access.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/vps_identity.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/sdk/write_hook.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/setup.cfg +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_activity_claude_runs.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_activity_collectors.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_activity_core.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_activity_live_tracking.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_activity_macos.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_activity_os_setup.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_activity_pipeline.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_agent_crud_cli.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_agent_sharing.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_approval_park.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_auto_ticket_queue.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_browser_profile.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_cheap_tier.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_claude_usage_gate.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_crash_recovery.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_create_safe_retry.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_deferred_tickets.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_done_transition.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_estimator.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_fallback_cli.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_fallback_cli_upgrade.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_fallback_rate_limit.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_faqs.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_github_fetch.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_graph_off_by_default.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_help_actions.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_help_attachments.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_help_questions.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_helpdesk.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_infographic_data.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_instructions_template.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_intake_job_outcome.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_journal.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_limit_fallback_handoff.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_local_exec_provider.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_log_colors_backlog.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_mcp_route_contract.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_memory_guard.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_model_policy.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_panel_help.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_panel_ticket_schedules.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_parallel_tickets.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_plan_ahead.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_pool_pr_modes.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_pr_identity.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_prd_artifact.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_project_path_resolution.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_providers.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_quiet_run_log.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_resume.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_router.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_run_verdict_is_honest.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_schedule_outputs_and_plans.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_schedule_tickets.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_schedule_timeline.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_scheduler_transient_retry.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_self_report.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_stale_panel_restart.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_swagger_command.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_theme_switch.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_ticket_board.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_ticket_bulk.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_ticket_cli.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_ticket_detail_view.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_ticket_plan.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_ticket_pools.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_ticket_schedule_and_agent.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_tier_flag_parity.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_ui_gate.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_ui_lifespan.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_ui_no_native_dialogs.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_updater_ignores_cwd.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_updater_pep668.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_updater_reports_the_truth.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_url_access.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_version_flag.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_vps_health.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_vps_only_reporting.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_windows_loop_identity.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_windows_panel_task.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_windows_stop_and_pidfile.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_windows_task_paths.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.45}/tests/test_write_hook.py +0 -0
|
@@ -279,6 +279,7 @@ tests/test_ticket_run_resolve_by_id.py
|
|
|
279
279
|
tests/test_ticket_schedule_and_agent.py
|
|
280
280
|
tests/test_ticket_speed_fixes.py
|
|
281
281
|
tests/test_tier_flag_parity.py
|
|
282
|
+
tests/test_ui_bruteforce_guard.py
|
|
282
283
|
tests/test_ui_gate.py
|
|
283
284
|
tests/test_ui_lifespan.py
|
|
284
285
|
tests/test_ui_no_native_dialogs.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
|
|
2
2
|
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.45
|
|
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.7.
|
|
37
|
+
__version__ = "1.7.45" # 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
|
|
@@ -19,10 +19,13 @@ from the *same* list the middleware enforces, so the two layers cannot drift.
|
|
|
19
19
|
agentos firewall --any --apply# open the port, leaving the allowlist as the gate
|
|
20
20
|
agentos firewall --revoke IP # print the matching delete rule
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
After a self-update ``updater._ensure_firewall`` runs :func:`ensure_restricted`:
|
|
23
|
+
it adds a per-IP rule for every allowlisted address and REMOVES the open-to-any
|
|
24
|
+
rule. It used to run ``--any``, and that left every VPS with 8765 open to the
|
|
25
|
+
internet: each brute-force/scanner request then reached Python before the
|
|
26
|
+
allowlist refused it, and a flood pinned the panel at 100% CPU. Open-to-any is
|
|
27
|
+
now only kept when the operator chose it — ``AGENTOS_UI_PUBLIC=1`` (public mode
|
|
28
|
+
needs the port reachable) or ``AGENTOS_FIREWALL_OPEN_ANY=1``.
|
|
26
29
|
|
|
27
30
|
Applying is deliberately opt-in: editing a live firewall is exactly the operation
|
|
28
31
|
that can lock an operator out of their own box, so the default is to print and let
|
|
@@ -30,6 +33,7 @@ a human read it first.
|
|
|
30
33
|
"""
|
|
31
34
|
from __future__ import annotations
|
|
32
35
|
|
|
36
|
+
import ipaddress
|
|
33
37
|
import os
|
|
34
38
|
import platform
|
|
35
39
|
import shutil
|
|
@@ -111,10 +115,13 @@ def allow_any_rules(port: int, system: str | None = None) -> list[str]:
|
|
|
111
115
|
corrected in seconds without root on the box, which is exactly what a
|
|
112
116
|
kernel rule cannot offer.
|
|
113
117
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
That reasoning held for *access* but not for *load*: every request from the
|
|
119
|
+
internet still costs a Python round-trip before the allowlist refuses it, and
|
|
120
|
+
a brute-force flood took panels to 100% CPU. So this is no longer the default
|
|
121
|
+
— :func:`ensure_restricted` is — and is kept for public mode and for an
|
|
122
|
+
operator who opts in with ``AGENTOS_FIREWALL_OPEN_ANY=1``. A backend IP move
|
|
123
|
+
is still self-healing: the new IP goes in ``_DEFAULT_ALLOWED_IPS`` and the
|
|
124
|
+
next self-update adds its rule.
|
|
118
125
|
"""
|
|
119
126
|
sysname = (system or _system()).lower()
|
|
120
127
|
|
|
@@ -201,6 +208,17 @@ def ensure_open(port: int | None = None, *, apply: bool = True) -> tuple[bool, s
|
|
|
201
208
|
who = "Administrator" if sysname == "windows" else "root (sudo)"
|
|
202
209
|
return (False, f"TCP {port} is not open and opening it needs {who}:\n{printable}")
|
|
203
210
|
|
|
211
|
+
failed = _apply_rules(rules)
|
|
212
|
+
if failed:
|
|
213
|
+
return (False, "could not open TCP %d: %s" % (port, "; ".join(failed)))
|
|
214
|
+
return (True, f"opened TCP {port} inbound")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _apply_rules(rules: list[str]) -> list[str]:
|
|
218
|
+
"""Run each rule (already privileged, so a printed ``sudo `` is stripped).
|
|
219
|
+
|
|
220
|
+
Returns one ``"<cmd>: <reason>"`` per failure — empty means all applied.
|
|
221
|
+
"""
|
|
204
222
|
failed: list[str] = []
|
|
205
223
|
for rule in rules:
|
|
206
224
|
cmd = rule[len("sudo "):] if rule.startswith("sudo ") else rule
|
|
@@ -214,9 +232,129 @@ def ensure_open(port: int | None = None, *, apply: bool = True) -> tuple[bool, s
|
|
|
214
232
|
if proc.returncode != 0:
|
|
215
233
|
tail = (proc.stderr or proc.stdout or "").strip().splitlines()
|
|
216
234
|
failed.append(f"{cmd}: {tail[-1][:120] if tail else f'exit {proc.returncode}'}")
|
|
235
|
+
return failed
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _net(value: str):
|
|
239
|
+
"""``value`` as an ip_network, or None — so ``1.2.3.4`` and ``1.2.3.4/32`` match."""
|
|
240
|
+
try:
|
|
241
|
+
return ipaddress.ip_network(value.strip(), strict=False)
|
|
242
|
+
except ValueError:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def open_any_opted_in() -> bool:
|
|
247
|
+
"""Has the operator chosen to keep the port open to any source?
|
|
248
|
+
|
|
249
|
+
True for ``AGENTOS_UI_PUBLIC=1`` (public mode is unreachable otherwise) or an
|
|
250
|
+
explicit ``AGENTOS_FIREWALL_OPEN_ANY=1``. Read from the process env or
|
|
251
|
+
``~/.agentos/.env``, like every other ``AGENTOS_UI_*`` toggle.
|
|
252
|
+
"""
|
|
253
|
+
try:
|
|
254
|
+
from sdk.ui.ip_allowlist import _TRUE, _setting, public_mode
|
|
255
|
+
except Exception: # noqa: BLE001 — never fail open on an import hiccup
|
|
256
|
+
return False
|
|
257
|
+
return public_mode() or _setting("AGENTOS_FIREWALL_OPEN_ANY").lower() in _TRUE
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def ensure_restricted(port: int | None = None, *, apply: bool = True,
|
|
261
|
+
ips: list[str] | None = None) -> tuple[bool, str]:
|
|
262
|
+
"""Limit ``port`` to the allowlisted IPs. Returns ``(changed, message)``. Never raises.
|
|
263
|
+
|
|
264
|
+
Adds a per-IP rule for each allowlisted address that lacks one, THEN removes
|
|
265
|
+
the open-to-any rule — in that order, and the removal is skipped if any add
|
|
266
|
+
failed, so the backend never loses its route mid-change. Same contract as
|
|
267
|
+
:func:`ensure_open` otherwise: unreadable state is never treated as closed,
|
|
268
|
+
an unprivileged run only returns the commands, and macOS never applies.
|
|
269
|
+
|
|
270
|
+
A message that needs the operator to act is multi-line (it carries commands),
|
|
271
|
+
which is what ``updater._ensure_firewall`` keys on to surface it.
|
|
272
|
+
"""
|
|
273
|
+
port = ui_port() if port is None else port
|
|
274
|
+
sysname = _system()
|
|
275
|
+
if ips is None:
|
|
276
|
+
try:
|
|
277
|
+
from sdk.config import get_allowed_ips
|
|
278
|
+
ips = get_allowed_ips()
|
|
279
|
+
except Exception: # noqa: BLE001
|
|
280
|
+
ips = []
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
sources = port_sources(port, sysname, collapse=False)
|
|
284
|
+
except Exception: # noqa: BLE001 — a status probe must never break an update
|
|
285
|
+
sources = None
|
|
286
|
+
|
|
287
|
+
inactive_note = ""
|
|
288
|
+
if sysname == "linux" and _ufw_inactive():
|
|
289
|
+
inactive_note = ("\nNOTE: ufw is INACTIVE, so no rule is enforced and the port is open "
|
|
290
|
+
"to everyone.\nEnable it once you have allowed SSH:\n"
|
|
291
|
+
" sudo ufw allow OpenSSH && sudo ufw enable")
|
|
292
|
+
|
|
293
|
+
if sources is None:
|
|
294
|
+
rules = allow_rules(ips, port, sysname) + [revoke_any_rule(port, sysname)]
|
|
295
|
+
printable = " " + "\n ".join(rules)
|
|
296
|
+
ok, tool = _tool_present()
|
|
297
|
+
why = tool if not ok else "needs root to read the firewall state"
|
|
298
|
+
return (False, f"could not read the firewall state ({why}) — to restrict TCP {port} "
|
|
299
|
+
f"to the allowlist yourself:\n{printable}")
|
|
300
|
+
|
|
301
|
+
have = {n for n in (_net(x) for x in sources) if n is not None}
|
|
302
|
+
missing = [ip for ip in ips if _net(ip) is not None and _net(ip) not in have]
|
|
303
|
+
open_any = "Anywhere" in sources
|
|
304
|
+
|
|
305
|
+
if not missing and not open_any:
|
|
306
|
+
return (False, f"TCP {port} is restricted to the allowlist" + inactive_note)
|
|
307
|
+
|
|
308
|
+
rules = allow_rules(missing, port, sysname)
|
|
309
|
+
if open_any:
|
|
310
|
+
rules.append(revoke_any_rule(port, sysname))
|
|
311
|
+
printable = " " + "\n ".join(rules)
|
|
312
|
+
what = (f"TCP {port} is open to ANY source" if open_any
|
|
313
|
+
else f"TCP {port} is missing a rule for: {', '.join(missing)}")
|
|
314
|
+
|
|
315
|
+
if not apply or sysname == "darwin":
|
|
316
|
+
return (False, f"{what}. To restrict it to the allowlist:\n{printable}" + inactive_note)
|
|
317
|
+
if not _is_privileged():
|
|
318
|
+
who = "Administrator" if sysname == "windows" else "root (sudo)"
|
|
319
|
+
return (False, f"{what} and restricting it needs {who}:\n{printable}" + inactive_note)
|
|
320
|
+
|
|
321
|
+
failed = _apply_rules(allow_rules(missing, port, sysname))
|
|
217
322
|
if failed:
|
|
218
|
-
|
|
219
|
-
|
|
323
|
+
# The any-rule stays: removing it now could cut the backend off.
|
|
324
|
+
return (False, f"could not add allowlist rules for TCP {port}: {'; '.join(failed)}"
|
|
325
|
+
f"\nThe open-to-any rule was left in place.")
|
|
326
|
+
if open_any:
|
|
327
|
+
failed = _apply_rules([revoke_any_rule(port, sysname)])
|
|
328
|
+
still = port_sources(port, sysname, collapse=False) or set()
|
|
329
|
+
if failed or "Anywhere" in still:
|
|
330
|
+
return (bool(missing), f"TCP {port} is still open to any source — a rule the SDK "
|
|
331
|
+
f"did not create allows it. Find and delete it:\n "
|
|
332
|
+
+ ("sudo ufw status numbered" if sysname == "linux" else
|
|
333
|
+
"netsh advfirewall firewall show rule name=all dir=in")
|
|
334
|
+
+ inactive_note)
|
|
335
|
+
return (True, f"restricted TCP {port} to the allowlist ({len(ips)} IP(s))" + inactive_note)
|
|
336
|
+
return (True, f"added allowlist rule(s) for TCP {port}: {', '.join(missing)}" + inactive_note)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def apply_ip(ip: str, *, add: bool = True, port: int | None = None) -> tuple[bool, str]:
|
|
340
|
+
"""Add (or remove) the kernel rule for one runtime-allowlisted ``ip``. Never raises.
|
|
341
|
+
|
|
342
|
+
Called by the ``/api/ui/allowlist`` routes so a newly whitelisted IP is not
|
|
343
|
+
dropped by the host firewall until the next self-update. Applies only when
|
|
344
|
+
the panel is privileged and the port is not deliberately open to any source;
|
|
345
|
+
otherwise returns the command for the operator to run.
|
|
346
|
+
"""
|
|
347
|
+
port = ui_port() if port is None else port
|
|
348
|
+
sysname = _system()
|
|
349
|
+
rule = (allow_rules([ip], port, sysname)[0] if add else revoke_rule(ip, port, sysname))
|
|
350
|
+
if open_any_opted_in():
|
|
351
|
+
return (False, "port is open to any source by choice — no per-IP rule needed")
|
|
352
|
+
if sysname not in ("linux", "windows") or not _is_privileged():
|
|
353
|
+
return (False, f"run on the host to update its firewall:\n {rule}")
|
|
354
|
+
failed = _apply_rules([rule])
|
|
355
|
+
if failed:
|
|
356
|
+
return (False, f"firewall rule failed: {failed[0]}")
|
|
357
|
+
return (True, f"firewall rule {'added' if add else 'removed'} for {ip}")
|
|
220
358
|
|
|
221
359
|
|
|
222
360
|
def revoke_rule(ip: str, port: int, system: str | None = None) -> str:
|
|
@@ -310,7 +448,8 @@ def _parse_netsh(text: str, port: int) -> set[str]:
|
|
|
310
448
|
return out
|
|
311
449
|
|
|
312
450
|
|
|
313
|
-
def port_sources(port: int, system: str | None = None
|
|
451
|
+
def port_sources(port: int, system: str | None = None, *,
|
|
452
|
+
collapse: bool = True) -> set[str] | None:
|
|
314
453
|
"""Which sources the HOST firewall currently allows to ``port``.
|
|
315
454
|
|
|
316
455
|
``{"Anywhere"}`` means open to the world. **``None`` means the state could not
|
|
@@ -322,6 +461,9 @@ def port_sources(port: int, system: str | None = None) -> set[str] | None:
|
|
|
322
461
|
(`sdk.config.get_allowed_ips`) and the host firewall are separate layers, and
|
|
323
462
|
the interesting question is where they DISAGREE — an IP the app permits but the
|
|
324
463
|
kernel drops looks exactly like a broken backend.
|
|
464
|
+
|
|
465
|
+
``collapse=False`` keeps the per-IP sources alongside ``"Anywhere"`` — what
|
|
466
|
+
:func:`ensure_restricted` needs to know which IP rules already exist.
|
|
325
467
|
"""
|
|
326
468
|
sysname = (system or _system()).lower()
|
|
327
469
|
if sysname == "linux":
|
|
@@ -329,17 +471,27 @@ def port_sources(port: int, system: str | None = None) -> set[str] | None:
|
|
|
329
471
|
if text is None:
|
|
330
472
|
return None
|
|
331
473
|
got = _parse_ufw(text, port)
|
|
332
|
-
return {"Anywhere"} if "Anywhere" in got else got
|
|
474
|
+
return {"Anywhere"} if collapse and "Anywhere" in got else got
|
|
333
475
|
if sysname == "windows":
|
|
334
476
|
text = _run(["netsh", "advfirewall", "firewall", "show", "rule",
|
|
335
477
|
"name=all", "dir=in"], timeout=25.0)
|
|
336
478
|
if text is None:
|
|
337
479
|
return None
|
|
338
480
|
got = _parse_netsh(text, port)
|
|
339
|
-
return {"Anywhere"} if "Anywhere" in got else got
|
|
481
|
+
return {"Anywhere"} if collapse and "Anywhere" in got else got
|
|
340
482
|
return None # macOS pf and anything else: not readable this way
|
|
341
483
|
|
|
342
484
|
|
|
485
|
+
def _ufw_inactive() -> bool:
|
|
486
|
+
"""True only when ufw positively reports ``Status: inactive``.
|
|
487
|
+
|
|
488
|
+
Then every ufw rule is decoration and the port is open regardless — the
|
|
489
|
+
"per-IP rules are applied but the flood still arrives" case.
|
|
490
|
+
"""
|
|
491
|
+
text = _run(["ufw", "status"])
|
|
492
|
+
return bool(text) and "status: inactive" in text.lower()
|
|
493
|
+
|
|
494
|
+
|
|
343
495
|
def _is_privileged() -> bool:
|
|
344
496
|
if _system() == "windows":
|
|
345
497
|
try:
|
|
@@ -382,10 +534,9 @@ def cli(argv: list[str]) -> int:
|
|
|
382
534
|
print(f"Allowed : {', '.join(ips) if ips else '(none — loopback only)'}")
|
|
383
535
|
|
|
384
536
|
# `--any` opens the port to every source and leaves the decision to
|
|
385
|
-
# IPAllowlistMiddleware.
|
|
386
|
-
#
|
|
387
|
-
#
|
|
388
|
-
# before the "nothing to open" bail below.
|
|
537
|
+
# IPAllowlistMiddleware. NOT the default any more (see `allow_any_rules`):
|
|
538
|
+
# every internet scanner then reaches Python. It does NOT need an allowlist to
|
|
539
|
+
# be populated first, so it is checked before the "nothing to open" bail below.
|
|
389
540
|
if "--any" in argv or "--open" in argv:
|
|
390
541
|
current = port_sources(port, sysname)
|
|
391
542
|
if current:
|
|
@@ -403,7 +554,10 @@ def cli(argv: list[str]) -> int:
|
|
|
403
554
|
for r in rules:
|
|
404
555
|
print(f" {r}")
|
|
405
556
|
print("\nAccess is still gated by the app allowlist above — an unlisted IP is\n"
|
|
406
|
-
"refused by IPAllowlistMiddleware even with the port open
|
|
557
|
+
"refused by IPAllowlistMiddleware even with the port open.\n"
|
|
558
|
+
"WARNING: every scanner on the internet can then reach the panel process;\n"
|
|
559
|
+
"a brute-force flood costs CPU even though it is refused. The next\n"
|
|
560
|
+
"self-update narrows it back unless AGENTOS_FIREWALL_OPEN_ANY=1 is set.")
|
|
407
561
|
if "--apply" not in argv:
|
|
408
562
|
print("\n(dry run — nothing changed. Re-run with --apply to execute.)")
|
|
409
563
|
return 0
|
|
@@ -417,6 +571,9 @@ def cli(argv: list[str]) -> int:
|
|
|
417
571
|
return 0
|
|
418
572
|
|
|
419
573
|
rules = allow_rules(ips, port, sysname)
|
|
574
|
+
if "Anywhere" in (port_sources(port, sysname) or set()):
|
|
575
|
+
rules.append(revoke_any_rule(port, sysname))
|
|
576
|
+
print(f"\nTCP {port} is currently open to ANY source — the last rule removes that.")
|
|
420
577
|
print("\nFirewall rules for this host:\n")
|
|
421
578
|
for r in rules:
|
|
422
579
|
print(f" {r}")
|
|
@@ -445,27 +602,9 @@ def cli(argv: list[str]) -> int:
|
|
|
445
602
|
print(f"\nerror: --apply needs {who}.")
|
|
446
603
|
return 1
|
|
447
604
|
|
|
448
|
-
|
|
449
|
-
print()
|
|
450
|
-
|
|
451
|
-
# The printed form carries `sudo` for humans; we are already privileged.
|
|
452
|
-
cmd = r[len("sudo "):] if r.startswith("sudo ") else r
|
|
453
|
-
try:
|
|
454
|
-
proc = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
|
|
455
|
-
except Exception as exc: # noqa: BLE001 — never leave the firewall half-known
|
|
456
|
-
print(f" ✗ {cmd}\n {exc}")
|
|
457
|
-
failed += 1
|
|
458
|
-
continue
|
|
459
|
-
tail = (proc.stdout or proc.stderr or "").strip().splitlines()
|
|
460
|
-
note = tail[-1][:100] if tail else ""
|
|
461
|
-
if proc.returncode == 0:
|
|
462
|
-
print(f" ✓ {cmd}\n {note}" if note else f" ✓ {cmd}")
|
|
463
|
-
else:
|
|
464
|
-
print(f" ✗ {cmd} (exit {proc.returncode})\n {note}")
|
|
465
|
-
failed += 1
|
|
466
|
-
|
|
467
|
-
print(f"\n{len(rules) - failed}/{len(rules)} applied.")
|
|
468
|
-
return 1 if failed else 0
|
|
605
|
+
changed, msg = ensure_restricted(port, apply=True, ips=ips)
|
|
606
|
+
print(f"\n{msg}")
|
|
607
|
+
return 0 if changed or "restricted to the allowlist" in msg else 1
|
|
469
608
|
|
|
470
609
|
|
|
471
610
|
if __name__ == "__main__": # pragma: no cover
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
3
|
"area": "Control panel",
|
|
4
|
-
"changes_30d":
|
|
4
|
+
"changes_30d": 7,
|
|
5
5
|
"features_30d": 2,
|
|
6
6
|
"fixes_30d": 1,
|
|
7
|
-
"latest": "security:
|
|
8
|
-
"latest_pr":
|
|
9
|
-
"latest_date": "2026-10-
|
|
7
|
+
"latest": "security: restrict panel port to the allowlist, cheap refusals, auth-failure bans, connection cap",
|
|
8
|
+
"latest_pr": 188,
|
|
9
|
+
"latest_date": "2026-10-29"
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"area": "Help",
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"pr": 188,
|
|
4
|
+
"date": "2026-10-29",
|
|
5
|
+
"merged_at": "2026-10-29T17:17:14+05:00",
|
|
6
|
+
"type": "Other",
|
|
7
|
+
"area": "Control panel",
|
|
8
|
+
"title": "security: restrict panel port to the allowlist, cheap refusals, auth-failure bans, connection cap",
|
|
9
|
+
"version": "1.7.45",
|
|
10
|
+
"url": "https://github.com/consultancy-outfit/DevOpsBot-SDK/pull/188"
|
|
11
|
+
},
|
|
2
12
|
{
|
|
3
13
|
"pr": 187,
|
|
4
14
|
"date": "2026-10-28",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.7.45",
|
|
4
|
+
"commit": "1f30499",
|
|
5
|
+
"pushed_at": "2026-10-29T17:17:14+05:00",
|
|
6
|
+
"generated_at": "2026-10-29T12:18:17+00:00",
|
|
7
|
+
"commands": 58,
|
|
8
|
+
"commands_invalid": 0,
|
|
9
|
+
"commands_undocumented": 0,
|
|
10
|
+
"panel_pages": 11,
|
|
11
|
+
"tests": 2591,
|
|
12
|
+
"prs_7d": 29,
|
|
13
|
+
"prs_30d": 40,
|
|
14
|
+
"features_30d": 12,
|
|
15
|
+
"fixes_30d": 18
|
|
16
|
+
}
|
|
17
|
+
]
|
|
@@ -230,7 +230,9 @@ async def add_to_allowlist(body: AllowlistEntry) -> dict:
|
|
|
230
230
|
"""
|
|
231
231
|
from sdk.config import add_allowed_ip
|
|
232
232
|
|
|
233
|
-
|
|
233
|
+
allowed = add_allowed_ip(body.ip)
|
|
234
|
+
return {"added": body.ip, "allowed_ips": allowed,
|
|
235
|
+
"firewall": await _sync_allowlist(body.ip, add=True)}
|
|
234
236
|
|
|
235
237
|
|
|
236
238
|
@router.delete("/allowlist", dependencies=[_auth])
|
|
@@ -239,9 +241,32 @@ async def remove_from_allowlist(ip: str = Query(...)) -> dict:
|
|
|
239
241
|
from sdk.config import remove_allowed_ip, PermanentAllowlistEntry
|
|
240
242
|
|
|
241
243
|
try:
|
|
242
|
-
|
|
244
|
+
allowed = remove_allowed_ip(ip)
|
|
243
245
|
except PermanentAllowlistEntry as exc:
|
|
244
246
|
raise HTTPException(status_code=400, detail=str(exc))
|
|
247
|
+
return {"removed": ip, "allowed_ips": allowed,
|
|
248
|
+
"firewall": await _sync_allowlist(ip, add=False)}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
async def _sync_allowlist(ip: str, *, add: bool) -> str:
|
|
252
|
+
"""Make an allowlist edit take effect at both layers; return the firewall note.
|
|
253
|
+
|
|
254
|
+
The middleware caches the allowlist, so it is told to re-read. The host
|
|
255
|
+
firewall now only admits allowlisted IPs (`sdk.firewall.ensure_restricted`),
|
|
256
|
+
so a new IP also needs its kernel rule — applied when the panel is privileged,
|
|
257
|
+
otherwise the note carries the command. Off the event loop: it shells out.
|
|
258
|
+
"""
|
|
259
|
+
import asyncio
|
|
260
|
+
|
|
261
|
+
from sdk import firewall
|
|
262
|
+
from sdk.ui.ip_allowlist import invalidate
|
|
263
|
+
|
|
264
|
+
invalidate()
|
|
265
|
+
try:
|
|
266
|
+
_ok, msg = await asyncio.to_thread(firewall.apply_ip, ip, add=add)
|
|
267
|
+
except Exception as exc: # noqa: BLE001 — the allowlist change already stands
|
|
268
|
+
msg = f"firewall not updated: {exc}"
|
|
269
|
+
return msg
|
|
245
270
|
|
|
246
271
|
|
|
247
272
|
# ── GitHub trigger (PR-only / push-only; never merges) ───────────────────────
|