devops-bot-sdk 1.7.45__tar.gz → 1.7.47__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.45 → devops_bot_sdk-1.7.47}/PKG-INFO +1 -1
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/cli.py +18 -7
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/config.py +33 -67
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/faqs.py +10 -18
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/firewall.py +177 -56
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/help_data/areas.json +13 -4
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/help_data/commands.json +11 -1
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/help_data/help.json +5 -1
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/help_data/pages.json +1 -1
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/help_data/releases.json +20 -0
- devops_bot_sdk-1.7.47/sdk/help_data/snapshot.json +17 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/helpdesk_kb.md +43 -34
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/swagger.py +0 -6
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/api.py +47 -36
- devops_bot_sdk-1.7.47/sdk/ui/ip_allowlist.py +199 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/models.py +8 -6
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/server.py +11 -8
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/app.js +44 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/index.html +14 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/styles.css +9 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/updater.py +1 -1
- devops_bot_sdk-1.7.47/tests/test_backend_only_allowlist.py +155 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_firewall_open_on_update.py +70 -11
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_swagger_command.py +1 -8
- devops_bot_sdk-1.7.47/tests/test_ui_bruteforce_guard.py +94 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ui_gate.py +57 -73
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ui_lifespan.py +5 -5
- devops_bot_sdk-1.7.45/sdk/help_data/snapshot.json +0 -17
- devops_bot_sdk-1.7.45/sdk/ui/ip_allowlist.py +0 -389
- devops_bot_sdk-1.7.45/tests/test_ui_bruteforce_guard.py +0 -170
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/README.md +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/pyproject.toml +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/classifier.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/collectors/common.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/collectors/linux_auditd.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/collectors/macos_eslogger.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/collectors/windows_eventlog.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/models.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/os_setup.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/recorder.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/redact.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/settings.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/shipper.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/spool.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agent_sharing.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/browser_profile.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/claude_usage.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/client.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/custom_agents.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/deferred_tickets.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/estimator.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/fallback_handoff.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/github_fetch.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/help_actions.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/helpdesk.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/instructions_template.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/journal.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/log_colors.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/memory_guard.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/model_policy.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/prd_progress.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/providers.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/resume.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/router.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/schedule_tickets.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/schedule_timeline.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/scope_plan.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/self_report.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/self_report_local.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/task_types.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/test.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/test_env.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ticket_board.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ticket_bulk.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ticket_plan.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ticket_pools.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/tickets.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/tiers/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/tiers/mini_agent.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/faqs.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/help.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/swagger.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/url_access.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/vps_identity.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/write_hook.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/setup.cfg +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_activity_claude_runs.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_activity_collectors.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_activity_core.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_activity_live_tracking.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_activity_macos.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_activity_os_setup.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_activity_pipeline.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_crud_cli.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_sharing.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_approval_park.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_auto_ticket_queue.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_browser_profile.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_cheap_tier.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_claude_usage_gate.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_crash_recovery.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_create_safe_retry.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_deferred_tickets.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_done_transition.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_estimator.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_fallback_cli.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_fallback_cli_upgrade.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_fallback_rate_limit.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_faqs.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_github_fetch.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_graph_off_by_default.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_help_actions.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_help_attachments.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_help_questions.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_helpdesk.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_infographic_data.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_instructions_template.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_intake_job_outcome.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_journal.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_limit_fallback_handoff.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_local_exec_provider.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_log_colors_backlog.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_mcp_route_contract.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_memory_guard.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_model_policy.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_panel_help.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_panel_ticket_schedules.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_parallel_tickets.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_plan_ahead.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_pool_pr_modes.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_pr_identity.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_prd_artifact.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_project_path_resolution.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_providers.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_quiet_run_log.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_resume.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_router.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_run_verdict_is_honest.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_schedule_outputs_and_plans.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_schedule_tickets.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_schedule_timeline.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_scheduler_transient_retry.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_self_report.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_stale_panel_restart.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_theme_switch.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_board.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_bulk.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_cli.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_detail_view.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_plan.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_pools.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_schedule_and_agent.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_tier_flag_parity.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ui_no_native_dialogs.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_updater_ignores_cwd.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_updater_pep668.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_updater_reports_the_truth.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_url_access.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_version_flag.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_vps_health.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_vps_only_reporting.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_windows_loop_identity.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_windows_panel_task.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_windows_stop_and_pidfile.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_windows_task_paths.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_write_hook.py +0 -0
|
@@ -190,6 +190,7 @@ tests/test_allowed_tools_config.py
|
|
|
190
190
|
tests/test_approval_park.py
|
|
191
191
|
tests/test_auto_agent_dispatch.py
|
|
192
192
|
tests/test_auto_ticket_queue.py
|
|
193
|
+
tests/test_backend_only_allowlist.py
|
|
193
194
|
tests/test_browser_profile.py
|
|
194
195
|
tests/test_cheap_tier.py
|
|
195
196
|
tests/test_claude_usage_gate.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.47
|
|
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.47" # 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
|
|
@@ -104,11 +104,17 @@ Command surface:
|
|
|
104
104
|
default; `--apply` executes them and needs root / Administrator. The host
|
|
105
105
|
firewall is a separate layer the allowlist cannot reach, and the two drifting
|
|
106
106
|
apart is what silently blinded every VPS when the backend's egress IP moved.
|
|
107
|
-
`--
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
`--apply` also removes the open-to-any rule and any rule for an IP no
|
|
108
|
+
longer allowed; this is what runs automatically after a self-update.
|
|
109
|
+
`--any` opens the port to every source instead (not recommended: every
|
|
110
|
+
scanner then reaches the panel). See sdk.firewall.
|
|
111
|
+
|
|
112
|
+
agentos allow-ip [--clear]
|
|
113
|
+
Show who may reach the panel: this machine and the DevOps Bot backend only
|
|
114
|
+
— no outside IP (since 1.7.47). `agentos allow-ip <IP>` is refused (exit 2)
|
|
115
|
+
with the SSH-tunnel alternative (ssh -L 8765:127.0.0.1:8765 <user>@<server>).
|
|
116
|
+
`--clear` removes outside IPs an older SDK left in config.toml and syncs the
|
|
117
|
+
host firewall (that half needs root). See sdk.firewall.
|
|
112
118
|
|
|
113
119
|
agentos swagger [--open] [--json [PATH]] [--port N]
|
|
114
120
|
Where the panel's Swagger UI is and whether it can actually be reached:
|
|
@@ -223,6 +229,10 @@ def main() -> None:
|
|
|
223
229
|
_github_command(rest)
|
|
224
230
|
return
|
|
225
231
|
|
|
232
|
+
if noun == "allow-ip":
|
|
233
|
+
from sdk import firewall
|
|
234
|
+
sys.exit(firewall.allow_ip_cli(rest))
|
|
235
|
+
|
|
226
236
|
if noun == "firewall":
|
|
227
237
|
from sdk import firewall
|
|
228
238
|
sys.exit(firewall.cli(rest))
|
|
@@ -264,7 +274,7 @@ def main() -> None:
|
|
|
264
274
|
|
|
265
275
|
|
|
266
276
|
def _print_usage() -> None:
|
|
267
|
-
print("usage: agentos <configure|chat|agent|ticket|integration|default-agents|agentd|github|report|browser|fallback|help|doctor|firewall|activity-setup|swagger> ...")
|
|
277
|
+
print("usage: agentos <configure|chat|agent|ticket|integration|default-agents|agentd|github|report|browser|fallback|help|doctor|firewall|allow-ip|activity-setup|swagger> ...")
|
|
268
278
|
print()
|
|
269
279
|
print(' agentos version (or --version) # installed SDK version, Python and install path')
|
|
270
280
|
print(' agentos help "<your problem>" [--file PATH]… # DevOps Bot Help: reads your logs + the SDK (+ screenshots/logs you attach)')
|
|
@@ -284,7 +294,8 @@ def _print_usage() -> None:
|
|
|
284
294
|
print(" agentos report <submit|list|show ID> # report work Jira/GitHub never saw")
|
|
285
295
|
print(" agentos browser <login URL|status|logout> # sign in once; private links become readable")
|
|
286
296
|
print(" agentos fallback <install|login|status|logout> # the AI Draftsman CLI used when claude is not working")
|
|
287
|
-
print(" agentos
|
|
297
|
+
print(" agentos allow-ip [--clear] # who may reach the panel (no outside IP; use an SSH tunnel)")
|
|
298
|
+
print(" agentos firewall [--apply] [--revoke IP] [--port N] # limit the UI port to the allowlisted IPs")
|
|
288
299
|
print(" agentos firewall --any [--apply] # open the UI port to any source (allowlist is the gate)")
|
|
289
300
|
print(" agentos swagger [--open] [--json [PATH]] # where /docs is, and can you reach it")
|
|
290
301
|
|
|
@@ -227,36 +227,39 @@ def _parse_ip_list(raw: str) -> list[str]:
|
|
|
227
227
|
|
|
228
228
|
|
|
229
229
|
def get_allowed_ips() -> list[str]:
|
|
230
|
-
"""Client IPs permitted to reach the ``agentos-ui``
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
230
|
+
"""Client IPs permitted to reach the ``agentos-ui`` server: the built-in backend
|
|
231
|
+
IP(s) and nothing else.
|
|
232
|
+
|
|
233
|
+
No outside user IP is allowed (since 1.7.47 — 1.7.46 allowed one). The panel is
|
|
234
|
+
for this machine and the DevOps Bot backend: open it on the server, or from
|
|
235
|
+
another computer through an SSH tunnel (``ssh -L 8765:127.0.0.1:8765 …``),
|
|
236
|
+
which arrives as loopback. Loopback and this host's own IPs are always trusted
|
|
237
|
+
separately. Anything still configured in ``AGENTOS_UI_ALLOWED_IPS`` or
|
|
238
|
+
config.toml ``allowed_ips`` is ignored and reported by
|
|
239
|
+
:func:`ignored_allowed_ips`; the host firewall (`sdk.firewall.ensure_restricted`)
|
|
240
|
+
removes its rule.
|
|
241
|
+
"""
|
|
242
|
+
return list(_DEFAULT_ALLOWED_IPS)
|
|
235
243
|
|
|
236
|
-
Sources, merged (all optional), each accepting a comma/space-separated or
|
|
237
|
-
bracketed list of strings:
|
|
238
|
-
- ``.env`` key ``AGENTOS_UI_ALLOWED_IPS`` (``~/.agentos/.env`` or CWD ``.env``)
|
|
239
|
-
- process env ``AGENTOS_UI_ALLOWED_IPS``
|
|
240
|
-
- config.toml key ``allowed_ips``
|
|
241
244
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
"""
|
|
246
|
-
sources = [
|
|
247
|
-
",".join(_DEFAULT_ALLOWED_IPS), # built-in backend IP(s) — always allowed
|
|
248
|
-
_dotenv_value("AGENTOS_UI_ALLOWED_IPS"),
|
|
249
|
-
os.environ.get("AGENTOS_UI_ALLOWED_IPS", ""),
|
|
250
|
-
_load_raw().get("allowed_ips", ""),
|
|
251
|
-
]
|
|
245
|
+
def _configured_user_ips() -> list[str]:
|
|
246
|
+
"""Every non-backend entry configured anywhere, oldest source first:
|
|
247
|
+
``.env``, process env, then config.toml (the runtime / API-added one)."""
|
|
252
248
|
out: list[str] = []
|
|
253
|
-
for src in
|
|
249
|
+
for src in (_dotenv_value("AGENTOS_UI_ALLOWED_IPS"),
|
|
250
|
+
os.environ.get("AGENTOS_UI_ALLOWED_IPS", ""),
|
|
251
|
+
_load_raw().get("allowed_ips", "")):
|
|
254
252
|
for ip in _parse_ip_list(src):
|
|
255
|
-
if ip not in out:
|
|
253
|
+
if ip not in _DEFAULT_ALLOWED_IPS and ip not in out:
|
|
256
254
|
out.append(ip)
|
|
257
255
|
return out
|
|
258
256
|
|
|
259
257
|
|
|
258
|
+
def ignored_allowed_ips() -> list[str]:
|
|
259
|
+
"""Configured outside IPs/ranges that are NOT allowed — none ever is."""
|
|
260
|
+
return _configured_user_ips()
|
|
261
|
+
|
|
262
|
+
|
|
260
263
|
# Built-in baseline of tools a headless agent run may use. Kept here (not in
|
|
261
264
|
# local_exec) so it is one configurable setting rather than a literal buried in the
|
|
262
265
|
# spawn code: the set WILL grow, and a new tool must be addable without editing
|
|
@@ -311,26 +314,14 @@ class PermanentAllowlistEntry(ValueError):
|
|
|
311
314
|
|
|
312
315
|
|
|
313
316
|
def permanent_allowed_ips() -> list[str]:
|
|
314
|
-
"""IPs
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
These are the fixed baseline: they are ALWAYS allowed and **cannot be disabled**
|
|
318
|
-
through the API — change them by editing ``.env`` and restarting.
|
|
319
|
-
"""
|
|
320
|
-
out: list[str] = []
|
|
321
|
-
for src in (",".join(_DEFAULT_ALLOWED_IPS), # built-in backend IP(s) — always permanent
|
|
322
|
-
_dotenv_value("AGENTOS_UI_ALLOWED_IPS"),
|
|
323
|
-
os.environ.get("AGENTOS_UI_ALLOWED_IPS", "")):
|
|
324
|
-
for ip in _parse_ip_list(src):
|
|
325
|
-
if ip not in out:
|
|
326
|
-
out.append(ip)
|
|
327
|
-
return out
|
|
317
|
+
"""The IPs that are always allowed and cannot be disabled — the built-in
|
|
318
|
+
backend IP(s). ``AGENTOS_UI_ALLOWED_IPS`` no longer adds to them (1.7.47)."""
|
|
319
|
+
return list(_DEFAULT_ALLOWED_IPS)
|
|
328
320
|
|
|
329
321
|
|
|
330
322
|
def runtime_allowed_ips() -> list[str]:
|
|
331
|
-
"""IPs/CIDRs
|
|
332
|
-
|
|
333
|
-
allowlist until disabled."""
|
|
323
|
+
"""IPs/CIDRs left in config.toml ``allowed_ips`` by an older SDK. Ignored since
|
|
324
|
+
1.7.47 (see :func:`get_allowed_ips`); ``agentos allow-ip --clear`` removes them."""
|
|
334
325
|
return _parse_ip_list(_load_raw().get("allowed_ips", ""))
|
|
335
326
|
|
|
336
327
|
|
|
@@ -433,40 +424,15 @@ def journal_enabled() -> bool:
|
|
|
433
424
|
return flag(FLAG_JOURNAL) or tiered_routing_enabled()
|
|
434
425
|
|
|
435
426
|
|
|
436
|
-
def add_allowed_ip(ip: str) -> list[str]:
|
|
437
|
-
"""Whitelist an IP/CIDR at RUNTIME (persisted to config.toml ``allowed_ips``).
|
|
438
|
-
|
|
439
|
-
Idempotent. The middleware re-reads the allowlist per request, so the IP is
|
|
440
|
-
honoured immediately (no restart) and stays until disabled. An IP already
|
|
441
|
-
permanently whitelisted via ``.env`` is left as-is (no runtime duplicate).
|
|
442
|
-
Returns the merged allowlist.
|
|
443
|
-
"""
|
|
444
|
-
ip = (ip or "").strip()
|
|
445
|
-
if not ip:
|
|
446
|
-
raise ValueError("ip is required")
|
|
447
|
-
if ip in permanent_allowed_ips(): # already permanent — nothing to add
|
|
448
|
-
return get_allowed_ips()
|
|
449
|
-
raw = _load_raw() # preserves token_enc / base_url / etc.
|
|
450
|
-
entries = _parse_ip_list(raw.get("allowed_ips", ""))
|
|
451
|
-
if ip not in entries:
|
|
452
|
-
entries.append(ip)
|
|
453
|
-
raw["allowed_ips"] = ", ".join(entries)
|
|
454
|
-
_save_raw(raw)
|
|
455
|
-
return get_allowed_ips()
|
|
456
|
-
|
|
457
|
-
|
|
458
427
|
def remove_allowed_ip(ip: str) -> list[str]:
|
|
459
|
-
"""
|
|
428
|
+
"""Remove a leftover entry from config.toml ``allowed_ips`` (ignored anyway).
|
|
460
429
|
|
|
461
|
-
Raises :class:`PermanentAllowlistEntry`
|
|
462
|
-
``.env`` — those are never removable through the API (edit ``.env`` instead).
|
|
463
|
-
Returns the merged allowlist.
|
|
430
|
+
Raises :class:`PermanentAllowlistEntry` for a backend IP. Returns the allowlist.
|
|
464
431
|
"""
|
|
465
432
|
ip = (ip or "").strip()
|
|
466
433
|
if ip in permanent_allowed_ips():
|
|
467
434
|
raise PermanentAllowlistEntry(
|
|
468
|
-
f"{ip} is
|
|
469
|
-
"it cannot be disabled through the API; edit .env and restart to change it."
|
|
435
|
+
f"{ip} is a DevOps Bot backend IP — it is always allowed and cannot be removed."
|
|
470
436
|
)
|
|
471
437
|
raw = _load_raw()
|
|
472
438
|
entries = [x for x in _parse_ip_list(raw.get("allowed_ips", "")) if x != ip]
|
|
@@ -19,24 +19,16 @@ FAQS: list[tuple[str, str, str]] = [
|
|
|
19
19
|
"7. **Your co_ API token** — `agentos configure`\n"
|
|
20
20
|
"Then `agentos doctor` checks the lot."),
|
|
21
21
|
("Basics", "How do I open the control panel from another machine?",
|
|
22
|
-
"**
|
|
23
|
-
"
|
|
24
|
-
"**
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"`
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"**On a Windows server** (PowerShell): `(Invoke-RestMethod https://api.ipify.org)` for your IP; "
|
|
33
|
-
"`Add-Content \"$env:USERPROFILE\\.agentos\\.env\" \"AGENTOS_UI_ALLOWED_IPS=203.0.113.7\"` the first time, or "
|
|
34
|
-
"`(Get-Content $f) -replace '^(AGENTOS_UI_ALLOWED_IPS=.*)$','$1,203.0.113.7' | Set-Content $f` "
|
|
35
|
-
"(with `$f = \"$env:USERPROFILE\\.agentos\\.env\"`) to add to an existing line; "
|
|
36
|
-
"`agentos firewall --apply` in an Administrator PowerShell. In cmd: "
|
|
37
|
-
"`echo AGENTOS_UI_ALLOWED_IPS=203.0.113.7>> \"%USERPROFILE%\\.agentos\\.env\"` (no space before `>>`).\n"
|
|
38
|
-
"5. Browse `http://SERVER-IP:8765` and paste your co_ token. It works at once — no restart. "
|
|
39
|
-
"`agentos swagger` shows whether the port is reachable and which allowlisted IPs the firewall still blocks."),
|
|
22
|
+
"**Use an SSH tunnel** — no outside IP can be allowed; only the server itself and the DevOps Bot "
|
|
23
|
+
"backend reach the panel.\n"
|
|
24
|
+
"1. **Linux / macOS:** on your machine run `ssh -L 8765:127.0.0.1:8765 you@SERVER` and leave it open.\n"
|
|
25
|
+
"2. **Windows:** the same command in PowerShell (built-in OpenSSH client): "
|
|
26
|
+
"`ssh -L 8765:127.0.0.1:8765 you@SERVER`.\n"
|
|
27
|
+
"3. Browse `http://127.0.0.1:8765` on your machine — it arrives as loopback, so no IP to allow. "
|
|
28
|
+
"(Port 8765 busy locally? `ssh -L 9000:127.0.0.1:8765 …` and use `http://127.0.0.1:9000`.)\n"
|
|
29
|
+
"Or open `http://127.0.0.1:8765` on the server itself. `agentos allow-ip <IP>` is refused; "
|
|
30
|
+
"`agentos allow-ip` shows who may connect and `agentos allow-ip --clear` removes outside IPs an older "
|
|
31
|
+
"SDK left behind. A leaked token cannot open the panel to another address."),
|
|
40
32
|
("Basics", "How do I use devops-bot-sdk on another machine?",
|
|
41
33
|
"Install it there too — the same checklist (Python, git, SSH key, repo access, claude or ad), then "
|
|
42
34
|
"`pip install \"git+ssh://git@github.com/consultancy-outfit/DevOpsBot-SDK.git\"` and `agentos configure` "
|
|
@@ -24,8 +24,7 @@ it adds a per-IP rule for every allowlisted address and REMOVES the open-to-any
|
|
|
24
24
|
rule. It used to run ``--any``, and that left every VPS with 8765 open to the
|
|
25
25
|
internet: each brute-force/scanner request then reached Python before the
|
|
26
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
|
|
28
|
-
needs the port reachable) or ``AGENTOS_FIREWALL_OPEN_ANY=1``.
|
|
27
|
+
now only kept when the operator chose it with ``AGENTOS_FIREWALL_OPEN_ANY=1``.
|
|
29
28
|
|
|
30
29
|
Applying is deliberately opt-in: editing a live firewall is exactly the operation
|
|
31
30
|
that can lock an operator out of their own box, so the default is to print and let
|
|
@@ -118,8 +117,8 @@ def allow_any_rules(port: int, system: str | None = None) -> list[str]:
|
|
|
118
117
|
That reasoning held for *access* but not for *load*: every request from the
|
|
119
118
|
internet still costs a Python round-trip before the allowlist refuses it, and
|
|
120
119
|
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
|
|
122
|
-
|
|
120
|
+
— :func:`ensure_restricted` is — and is kept only for an operator who opts
|
|
121
|
+
in with ``AGENTOS_FIREWALL_OPEN_ANY=1``. A backend IP move
|
|
123
122
|
is still self-healing: the new IP goes in ``_DEFAULT_ALLOWED_IPS`` and the
|
|
124
123
|
next self-update adds its rule.
|
|
125
124
|
"""
|
|
@@ -159,6 +158,41 @@ def revoke_any_rule(port: int, system: str | None = None) -> str:
|
|
|
159
158
|
return f"# unsupported platform {sysname!r}"
|
|
160
159
|
|
|
161
160
|
|
|
161
|
+
def revoke_any_rules(port: int, system: str | None = None,
|
|
162
|
+
forms: list[str] | None = None) -> list[str]:
|
|
163
|
+
"""Every command needed to remove the open-to-any rule(s) for ``port``.
|
|
164
|
+
|
|
165
|
+
ufw keeps ``8765/tcp`` and a bare ``8765`` (tcp+udp, what a hand-typed
|
|
166
|
+
``ufw allow 8765`` makes) as DIFFERENT rules, and deleting one leaves the other
|
|
167
|
+
— the port then stays open to everyone after ``ufw delete allow 8765/tcp``.
|
|
168
|
+
``forms`` are the ``To`` spellings ufw shows for the Anywhere rows; unknown
|
|
169
|
+
(None) → both, since deleting a rule that does not exist is harmless.
|
|
170
|
+
"""
|
|
171
|
+
sysname = (system or _system()).lower()
|
|
172
|
+
if sysname != "linux":
|
|
173
|
+
return [revoke_any_rule(port, sysname)]
|
|
174
|
+
if forms is None:
|
|
175
|
+
forms = [f"{port}/tcp", str(port)]
|
|
176
|
+
return [f"sudo ufw delete allow {f}" for f in forms]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _ufw_any_forms(port: int) -> list[str] | None:
|
|
180
|
+
"""The ``To`` spellings of the IPv4 open-to-any rows for ``port`` (e.g.
|
|
181
|
+
``["8765/tcp", "8765"]``), or None when ufw cannot be read."""
|
|
182
|
+
text = _run(_ufw_argv() + ["status"])
|
|
183
|
+
if text is None:
|
|
184
|
+
return None
|
|
185
|
+
out: list[str] = []
|
|
186
|
+
for line in text.splitlines():
|
|
187
|
+
parts = line.split("#", 1)[0].split()
|
|
188
|
+
if (len(parts) >= 3 and "(v6)" not in line and parts[1].upper() == "ALLOW"
|
|
189
|
+
and parts[-1] == "Anywhere"
|
|
190
|
+
and (parts[0] == str(port) or parts[0].startswith(f"{port}/"))
|
|
191
|
+
and parts[0] not in out):
|
|
192
|
+
out.append(parts[0])
|
|
193
|
+
return out
|
|
194
|
+
|
|
195
|
+
|
|
162
196
|
def ensure_open(port: int | None = None, *, apply: bool = True) -> tuple[bool, str]:
|
|
163
197
|
"""Make sure ``port`` is reachable. Returns ``(changed, message)``. Never raises.
|
|
164
198
|
|
|
@@ -198,7 +232,7 @@ def ensure_open(port: int | None = None, *, apply: bool = True) -> tuple[bool, s
|
|
|
198
232
|
return (False, f"macOS: pf state is not readable and pf rules are file-based —\n{printable}")
|
|
199
233
|
if sources is None:
|
|
200
234
|
ok, tool = _tool_present()
|
|
201
|
-
why = tool if not ok else "needs root to read the firewall state"
|
|
235
|
+
why = tool if not ok else "needs root or passwordless sudo to read the firewall state"
|
|
202
236
|
return (False, f"could not read the firewall state ({why}) — to open it yourself:\n{printable}")
|
|
203
237
|
|
|
204
238
|
# Readable and genuinely empty for this port.
|
|
@@ -215,13 +249,16 @@ def ensure_open(port: int | None = None, *, apply: bool = True) -> tuple[bool, s
|
|
|
215
249
|
|
|
216
250
|
|
|
217
251
|
def _apply_rules(rules: list[str]) -> list[str]:
|
|
218
|
-
"""Run each rule
|
|
252
|
+
"""Run each rule. As root a printed ``sudo `` is stripped; otherwise it becomes
|
|
253
|
+
``sudo -n `` (passwordless sudo — see :func:`_sudo_n_ok`), which fails instead
|
|
254
|
+
of waiting for a password.
|
|
219
255
|
|
|
220
256
|
Returns one ``"<cmd>: <reason>"`` per failure — empty means all applied.
|
|
221
257
|
"""
|
|
258
|
+
prefix = "sudo -n " if _sudo_n_ok() else ""
|
|
222
259
|
failed: list[str] = []
|
|
223
260
|
for rule in rules:
|
|
224
|
-
cmd = rule[len("sudo "):] if rule.startswith("sudo ") else rule
|
|
261
|
+
cmd = prefix + rule[len("sudo "):] if rule.startswith("sudo ") else rule
|
|
225
262
|
try:
|
|
226
263
|
proc = subprocess.run(
|
|
227
264
|
cmd, shell=True, capture_output=True, text=True, timeout=30,
|
|
@@ -246,24 +283,27 @@ def _net(value: str):
|
|
|
246
283
|
def open_any_opted_in() -> bool:
|
|
247
284
|
"""Has the operator chosen to keep the port open to any source?
|
|
248
285
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
286
|
+
Only an explicit ``AGENTOS_FIREWALL_OPEN_ANY=1`` — read from the process env
|
|
287
|
+
or ``~/.agentos/.env``, like every other ``AGENTOS_UI_*`` toggle. (The removed
|
|
288
|
+
``AGENTOS_UI_PUBLIC=1`` used to imply it.)
|
|
252
289
|
"""
|
|
253
290
|
try:
|
|
254
|
-
from sdk.ui.ip_allowlist import _TRUE, _setting
|
|
291
|
+
from sdk.ui.ip_allowlist import _TRUE, _setting
|
|
255
292
|
except Exception: # noqa: BLE001 — never fail open on an import hiccup
|
|
256
293
|
return False
|
|
257
|
-
return
|
|
294
|
+
return _setting("AGENTOS_FIREWALL_OPEN_ANY").lower() in _TRUE
|
|
258
295
|
|
|
259
296
|
|
|
260
297
|
def ensure_restricted(port: int | None = None, *, apply: bool = True,
|
|
261
298
|
ips: list[str] | None = None) -> tuple[bool, str]:
|
|
262
|
-
"""Limit ``port`` to the allowlisted IPs. Returns ``(changed, message)``.
|
|
299
|
+
"""Limit ``port`` to EXACTLY the allowlisted IPs. Returns ``(changed, message)``.
|
|
300
|
+
Never raises.
|
|
263
301
|
|
|
264
302
|
Adds a per-IP rule for each allowlisted address that lacks one, THEN removes
|
|
265
|
-
the open-to-any rule
|
|
266
|
-
|
|
303
|
+
the open-to-any rule and every per-IP rule for an address no longer allowed
|
|
304
|
+
(only one user IP may reach the panel, so replacing it must close the old
|
|
305
|
+
one) — in that order, and the removals are skipped if any add failed, so the
|
|
306
|
+
backend never loses its route mid-change. Same contract as
|
|
267
307
|
:func:`ensure_open` otherwise: unreadable state is never treated as closed,
|
|
268
308
|
an unprivileged run only returns the commands, and macOS never applies.
|
|
269
309
|
|
|
@@ -291,26 +331,35 @@ def ensure_restricted(port: int | None = None, *, apply: bool = True,
|
|
|
291
331
|
" sudo ufw allow OpenSSH && sudo ufw enable")
|
|
292
332
|
|
|
293
333
|
if sources is None:
|
|
294
|
-
rules = allow_rules(ips, port, sysname) +
|
|
334
|
+
rules = allow_rules(ips, port, sysname) + revoke_any_rules(port, sysname)
|
|
295
335
|
printable = " " + "\n ".join(rules)
|
|
296
336
|
ok, tool = _tool_present()
|
|
297
|
-
why = tool if not ok else "needs root to read the firewall state"
|
|
337
|
+
why = tool if not ok else "needs root or passwordless sudo to read the firewall state"
|
|
298
338
|
return (False, f"could not read the firewall state ({why}) — to restrict TCP {port} "
|
|
299
339
|
f"to the allowlist yourself:\n{printable}")
|
|
300
340
|
|
|
301
341
|
have = {n for n in (_net(x) for x in sources) if n is not None}
|
|
342
|
+
want = {n for n in (_net(ip) for ip in ips) if n is not None}
|
|
302
343
|
missing = [ip for ip in ips if _net(ip) is not None and _net(ip) not in have]
|
|
344
|
+
stale = sorted(x for x in sources if _net(x) is not None and _net(x) not in want)
|
|
303
345
|
open_any = "Anywhere" in sources
|
|
304
346
|
|
|
305
|
-
if not missing and not open_any:
|
|
347
|
+
if not missing and not open_any and not stale:
|
|
306
348
|
return (False, f"TCP {port} is restricted to the allowlist" + inactive_note)
|
|
307
349
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
350
|
+
# Read ufw's exact spelling only when the deletes will run; a printed list
|
|
351
|
+
# names both forms (deleting a rule that is not there is harmless).
|
|
352
|
+
any_forms = (_ufw_any_forms(port) or [f"{port}/tcp"]) if (
|
|
353
|
+
sysname == "linux" and open_any and apply and _is_privileged()) else None
|
|
354
|
+
removals = (revoke_any_rules(port, sysname, any_forms) if open_any else []) + [
|
|
355
|
+
revoke_rule(x, port, sysname) for x in stale]
|
|
356
|
+
rules = allow_rules(missing, port, sysname) + removals
|
|
311
357
|
printable = " " + "\n ".join(rules)
|
|
312
|
-
what =
|
|
313
|
-
|
|
358
|
+
what = "; ".join(w for w in (
|
|
359
|
+
f"TCP {port} is open to ANY source" if open_any else "",
|
|
360
|
+
f"missing a rule for: {', '.join(missing)}" if missing else "",
|
|
361
|
+
f"still allows: {', '.join(stale)}" if stale else "",
|
|
362
|
+
) if w)
|
|
314
363
|
|
|
315
364
|
if not apply or sysname == "darwin":
|
|
316
365
|
return (False, f"{what}. To restrict it to the allowlist:\n{printable}" + inactive_note)
|
|
@@ -323,38 +372,31 @@ def ensure_restricted(port: int | None = None, *, apply: bool = True,
|
|
|
323
372
|
# The any-rule stays: removing it now could cut the backend off.
|
|
324
373
|
return (False, f"could not add allowlist rules for TCP {port}: {'; '.join(failed)}"
|
|
325
374
|
f"\nThe open-to-any rule was left in place.")
|
|
326
|
-
if
|
|
327
|
-
|
|
375
|
+
if removals:
|
|
376
|
+
_apply_rules(removals)
|
|
328
377
|
still = port_sources(port, sysname, collapse=False) or set()
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
378
|
+
left = sorted(x for x in still
|
|
379
|
+
if x == "Anywhere" or (_net(x) is not None and _net(x) not in want))
|
|
380
|
+
if left:
|
|
381
|
+
return (True, f"TCP {port} still allows {', '.join(left)} — a rule the SDK did "
|
|
382
|
+
f"not create. Find and delete it:\n "
|
|
383
|
+
+ ("sudo ufw status numbered" if sysname == "linux" else
|
|
384
|
+
"netsh advfirewall firewall show rule name=all dir=in")
|
|
385
|
+
+ inactive_note)
|
|
386
|
+
return (True, f"restricted TCP {port} to the allowlist: {', '.join(ips)}" + inactive_note)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def sync_rules(port: int | None = None) -> tuple[bool, str]:
|
|
390
|
+
"""Bring the host firewall in line with the allowlist after it changed.
|
|
391
|
+
|
|
392
|
+
Called when the one user IP is set or cleared (CLI or panel), so the new IP's
|
|
393
|
+
rule is added and the old one's removed straight away rather than at the next
|
|
394
|
+
self-update. Leaves a deliberately open port alone. Never raises.
|
|
346
395
|
"""
|
|
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
396
|
if open_any_opted_in():
|
|
351
|
-
return (False, "port is open to any source by choice —
|
|
352
|
-
|
|
353
|
-
|
|
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}")
|
|
397
|
+
return (False, "port is open to any source by choice (AGENTOS_FIREWALL_OPEN_ANY) — "
|
|
398
|
+
"no per-IP rules to change")
|
|
399
|
+
return ensure_restricted(port)
|
|
358
400
|
|
|
359
401
|
|
|
360
402
|
def revoke_rule(ip: str, port: int, system: str | None = None) -> str:
|
|
@@ -467,7 +509,7 @@ def port_sources(port: int, system: str | None = None, *,
|
|
|
467
509
|
"""
|
|
468
510
|
sysname = (system or _system()).lower()
|
|
469
511
|
if sysname == "linux":
|
|
470
|
-
text = _run(["
|
|
512
|
+
text = _run(_ufw_argv() + ["status"])
|
|
471
513
|
if text is None:
|
|
472
514
|
return None
|
|
473
515
|
got = _parse_ufw(text, port)
|
|
@@ -488,10 +530,42 @@ def _ufw_inactive() -> bool:
|
|
|
488
530
|
Then every ufw rule is decoration and the port is open regardless — the
|
|
489
531
|
"per-IP rules are applied but the flood still arrives" case.
|
|
490
532
|
"""
|
|
491
|
-
text = _run(["
|
|
533
|
+
text = _run(_ufw_argv() + ["status"])
|
|
492
534
|
return bool(text) and "status: inactive" in text.lower()
|
|
493
535
|
|
|
494
536
|
|
|
537
|
+
def _is_root() -> bool:
|
|
538
|
+
return hasattr(os, "geteuid") and os.geteuid() == 0
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
_SUDO_N: bool | None = None
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _sudo_n_ok() -> bool:
|
|
545
|
+
"""True when this (non-root, Linux) user may run ufw through ``sudo -n``.
|
|
546
|
+
|
|
547
|
+
``agentos-auto update`` runs as the panel user, not root, so without this the
|
|
548
|
+
post-update hook could neither read nor fix ufw and only ever printed the
|
|
549
|
+
commands — the port stayed open to the world between updates. ``-n`` never
|
|
550
|
+
prompts, so an unattended update still cannot stall waiting for a password.
|
|
551
|
+
Probed once per process.
|
|
552
|
+
"""
|
|
553
|
+
global _SUDO_N
|
|
554
|
+
if _SUDO_N is None:
|
|
555
|
+
_SUDO_N = False
|
|
556
|
+
if _system() == "linux" and not _is_root() and shutil.which("sudo") and shutil.which("ufw"):
|
|
557
|
+
try:
|
|
558
|
+
_SUDO_N = subprocess.run(["sudo", "-n", "ufw", "status"], capture_output=True,
|
|
559
|
+
text=True, timeout=10).returncode == 0
|
|
560
|
+
except Exception: # noqa: BLE001
|
|
561
|
+
_SUDO_N = False
|
|
562
|
+
return _SUDO_N
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _ufw_argv() -> list[str]:
|
|
566
|
+
return ["sudo", "-n", "ufw"] if _sudo_n_ok() else ["ufw"]
|
|
567
|
+
|
|
568
|
+
|
|
495
569
|
def _is_privileged() -> bool:
|
|
496
570
|
if _system() == "windows":
|
|
497
571
|
try:
|
|
@@ -499,7 +573,7 @@ def _is_privileged() -> bool:
|
|
|
499
573
|
return bool(ctypes.windll.shell32.IsUserAnAdmin())
|
|
500
574
|
except Exception: # noqa: BLE001 — not Windows, or no ctypes
|
|
501
575
|
return False
|
|
502
|
-
return
|
|
576
|
+
return _is_root() or _sudo_n_ok()
|
|
503
577
|
|
|
504
578
|
|
|
505
579
|
def cli(argv: list[str]) -> int:
|
|
@@ -572,7 +646,8 @@ def cli(argv: list[str]) -> int:
|
|
|
572
646
|
|
|
573
647
|
rules = allow_rules(ips, port, sysname)
|
|
574
648
|
if "Anywhere" in (port_sources(port, sysname) or set()):
|
|
575
|
-
rules.
|
|
649
|
+
rules.extend(revoke_any_rules(
|
|
650
|
+
port, sysname, _ufw_any_forms(port) if sysname == "linux" else None))
|
|
576
651
|
print(f"\nTCP {port} is currently open to ANY source — the last rule removes that.")
|
|
577
652
|
print("\nFirewall rules for this host:\n")
|
|
578
653
|
for r in rules:
|
|
@@ -607,5 +682,51 @@ def cli(argv: list[str]) -> int:
|
|
|
607
682
|
return 0 if changed or "restricted to the allowlist" in msg else 1
|
|
608
683
|
|
|
609
684
|
|
|
685
|
+
def allow_ip_cli(argv: list[str]) -> int:
|
|
686
|
+
"""``agentos allow-ip [--clear]`` — show who may reach the panel, or clear the
|
|
687
|
+
outside IPs an older SDK left configured.
|
|
688
|
+
|
|
689
|
+
Since 1.7.47 no outside IP is allowed — only this machine and the DevOps Bot
|
|
690
|
+
backend — so setting one is refused with the SSH-tunnel alternative.
|
|
691
|
+
``--clear`` empties config.toml ``allowed_ips`` and syncs the host firewall
|
|
692
|
+
(which drops their rules); ``.env`` entries are reported for the operator to
|
|
693
|
+
delete.
|
|
694
|
+
"""
|
|
695
|
+
from sdk import config
|
|
696
|
+
|
|
697
|
+
def show() -> None:
|
|
698
|
+
print(f"Allowed : this machine + DevOps Bot backend "
|
|
699
|
+
f"({', '.join(config._DEFAULT_ALLOWED_IPS)}) — no outside IP")
|
|
700
|
+
ignored = config.ignored_allowed_ips()
|
|
701
|
+
if ignored:
|
|
702
|
+
print(f"Ignored : {', '.join(ignored)} (outside IPs are not allowed; "
|
|
703
|
+
"`agentos allow-ip --clear` removes them)")
|
|
704
|
+
|
|
705
|
+
args = [a for a in argv if a.strip()]
|
|
706
|
+
if not args or args[0] in ("--show", "show"):
|
|
707
|
+
show()
|
|
708
|
+
return 0
|
|
709
|
+
|
|
710
|
+
if args[0] not in ("--clear", "clear", "--remove"):
|
|
711
|
+
print("error: outside IPs can no longer be allowed — only this machine and the "
|
|
712
|
+
"DevOps Bot backend reach the panel.\n"
|
|
713
|
+
"From another computer use an SSH tunnel:\n"
|
|
714
|
+
" ssh -L 8765:127.0.0.1:8765 <user>@<server> then open http://127.0.0.1:8765")
|
|
715
|
+
return 2
|
|
716
|
+
|
|
717
|
+
for ip in config.runtime_allowed_ips():
|
|
718
|
+
if ip not in config._DEFAULT_ALLOWED_IPS:
|
|
719
|
+
config.remove_allowed_ip(ip)
|
|
720
|
+
print(f"✓ removed {ip} from ~/.agentos/config.toml")
|
|
721
|
+
left = config.ignored_allowed_ips()
|
|
722
|
+
if left:
|
|
723
|
+
print(f"Still in AGENTOS_UI_ALLOWED_IPS (~/.agentos/.env or the environment): "
|
|
724
|
+
f"{', '.join(left)} — ignored; delete that line to tidy up.")
|
|
725
|
+
changed, msg = sync_rules()
|
|
726
|
+
print(("↻ " if changed else "") + msg)
|
|
727
|
+
show()
|
|
728
|
+
return 0
|
|
729
|
+
|
|
730
|
+
|
|
610
731
|
if __name__ == "__main__": # pragma: no cover
|
|
611
732
|
sys.exit(cli(sys.argv[1:]))
|