devops-bot-sdk 1.7.43__tar.gz → 1.7.46__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.46}/PKG-INFO +1 -1
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/SOURCES.txt +2 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/cli.py +18 -7
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/config.py +59 -31
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/faqs.py +11 -16
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/firewall.py +233 -38
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/help_data/areas.json +13 -4
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/help_data/commands.json +11 -1
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/help_data/help.json +5 -1
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/help_data/pages.json +1 -1
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/help_data/releases.json +20 -0
- devops_bot_sdk-1.7.46/sdk/help_data/snapshot.json +17 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/helpdesk_kb.md +45 -33
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/swagger.py +0 -6
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/api.py +79 -19
- devops_bot_sdk-1.7.46/sdk/ui/ip_allowlist.py +199 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/models.py +8 -6
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/security.py +22 -2
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/server.py +26 -9
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/static/app.js +62 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/static/index.html +22 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/static/styles.css +9 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/updater.py +13 -5
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_firewall_open_on_update.py +147 -15
- devops_bot_sdk-1.7.46/tests/test_one_allowed_ip.py +153 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_swagger_command.py +1 -8
- devops_bot_sdk-1.7.46/tests/test_ui_bruteforce_guard.py +94 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_ui_gate.py +57 -73
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_ui_lifespan.py +5 -5
- devops_bot_sdk-1.7.43/sdk/help_data/snapshot.json +0 -17
- devops_bot_sdk-1.7.43/sdk/ui/ip_allowlist.py +0 -258
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/README.md +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/pyproject.toml +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/activity/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/activity/classifier.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/activity/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/activity/collectors/common.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/activity/collectors/linux_auditd.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/activity/collectors/macos_eslogger.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/activity/collectors/windows_eventlog.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/activity/models.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/activity/os_setup.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/activity/recorder.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/activity/redact.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/activity/settings.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/activity/shipper.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/activity/spool.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agent_sharing.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/browser_profile.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/claude_usage.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/client.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/custom_agents.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/deferred_tickets.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/estimator.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/fallback_handoff.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/github_fetch.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/help_actions.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/helpdesk.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/instructions_template.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/journal.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/log_colors.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/memory_guard.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/model_policy.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/prd_progress.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/providers.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/resume.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/router.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/schedule_tickets.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/schedule_timeline.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/scope_plan.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/self_report.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/self_report_local.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/task_types.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/test.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/test_env.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ticket_board.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ticket_bulk.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ticket_plan.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ticket_pools.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/tickets.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/tiers/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/tiers/mini_agent.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/static/faqs.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/static/help.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/ui/swagger.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/url_access.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/vps_identity.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/sdk/write_hook.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/setup.cfg +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_activity_claude_runs.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_activity_collectors.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_activity_core.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_activity_live_tracking.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_activity_macos.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_activity_os_setup.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_activity_pipeline.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_agent_crud_cli.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_agent_sharing.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_approval_park.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_auto_ticket_queue.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_browser_profile.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_cheap_tier.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_claude_usage_gate.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_crash_recovery.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_create_safe_retry.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_deferred_tickets.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_done_transition.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_estimator.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_fallback_cli.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_fallback_cli_upgrade.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_fallback_rate_limit.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_faqs.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_github_fetch.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_graph_off_by_default.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_help_actions.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_help_attachments.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_help_questions.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_helpdesk.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_infographic_data.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_instructions_template.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_intake_job_outcome.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_journal.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_limit_fallback_handoff.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_local_exec_provider.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_log_colors_backlog.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_mcp_route_contract.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_memory_guard.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_model_policy.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_panel_help.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_panel_ticket_schedules.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_parallel_tickets.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_plan_ahead.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_pool_pr_modes.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_pr_identity.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_prd_artifact.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_project_path_resolution.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_providers.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_quiet_run_log.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_resume.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_router.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_run_verdict_is_honest.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_schedule_outputs_and_plans.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_schedule_tickets.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_schedule_timeline.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_scheduler_transient_retry.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_self_report.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_stale_panel_restart.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_theme_switch.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_ticket_board.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_ticket_bulk.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_ticket_cli.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_ticket_detail_view.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_ticket_plan.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_ticket_pools.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_ticket_schedule_and_agent.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_tier_flag_parity.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_ui_no_native_dialogs.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_updater_ignores_cwd.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_updater_pep668.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_updater_reports_the_truth.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_url_access.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_version_flag.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_vps_health.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_vps_only_reporting.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_windows_loop_identity.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_windows_panel_task.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_windows_stop_and_pidfile.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_windows_task_paths.py +0 -0
- {devops_bot_sdk-1.7.43 → devops_bot_sdk-1.7.46}/tests/test_write_hook.py +0 -0
|
@@ -235,6 +235,7 @@ tests/test_log_colors_backlog.py
|
|
|
235
235
|
tests/test_mcp_route_contract.py
|
|
236
236
|
tests/test_memory_guard.py
|
|
237
237
|
tests/test_model_policy.py
|
|
238
|
+
tests/test_one_allowed_ip.py
|
|
238
239
|
tests/test_panel_help.py
|
|
239
240
|
tests/test_panel_ticket_schedules.py
|
|
240
241
|
tests/test_parallel_tickets.py
|
|
@@ -279,6 +280,7 @@ tests/test_ticket_run_resolve_by_id.py
|
|
|
279
280
|
tests/test_ticket_schedule_and_agent.py
|
|
280
281
|
tests/test_ticket_speed_fixes.py
|
|
281
282
|
tests/test_tier_flag_parity.py
|
|
283
|
+
tests/test_ui_bruteforce_guard.py
|
|
282
284
|
tests/test_ui_gate.py
|
|
283
285
|
tests/test_ui_lifespan.py
|
|
284
286
|
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.46
|
|
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.46" # 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 [IP | --clear]
|
|
113
|
+
Show, set or clear THE one outside IP allowed to reach the panel (besides
|
|
114
|
+
this machine and the backend). Only one at a time: a new IP replaces the
|
|
115
|
+
old one in the allowlist and the host firewall. Same-machine only — this
|
|
116
|
+
command over SSH, or the panel opened at http://127.0.0.1:8765 on the box;
|
|
117
|
+
the remote API refuses. The firewall 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 [IP | --clear] # the ONE outside IP allowed to reach the panel")
|
|
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,64 @@ 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``
|
|
230
|
+
"""Client IPs permitted to reach the ``agentos-ui`` server: the built-in backend
|
|
231
|
+
IP(s) plus AT MOST ONE user IP.
|
|
231
232
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
ONE user IP at a time is the rule — the panel is reachable from a single
|
|
234
|
+
outside address, so a leaked token is useless anywhere else and the host
|
|
235
|
+
firewall (`sdk.firewall.ensure_restricted`) admits exactly that address.
|
|
236
|
+
Loopback and this host's own IPs are always trusted separately.
|
|
235
237
|
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
242
|
-
Returns a de-duplicated list (empty means "loopback only"). Supplying the
|
|
243
|
-
AI-backend IP here later is all that's needed to open the link — no code
|
|
244
|
-
change. CIDR/prefix entries are supported by the middleware.
|
|
238
|
+
The user IP is chosen by :func:`user_allowed_ip`; anything else configured is
|
|
239
|
+
ignored and reported by :func:`ignored_allowed_ips`.
|
|
245
240
|
"""
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
241
|
+
out = list(_DEFAULT_ALLOWED_IPS)
|
|
242
|
+
user = user_allowed_ip()
|
|
243
|
+
if user and user not in out:
|
|
244
|
+
out.append(user)
|
|
245
|
+
return out
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _single_address(value: str) -> bool:
|
|
249
|
+
"""True for one host address (a bare IP, or a /32 / /128) — not a range."""
|
|
250
|
+
import ipaddress
|
|
251
|
+
try:
|
|
252
|
+
return ipaddress.ip_network(value.strip(), strict=False).num_addresses == 1
|
|
253
|
+
except ValueError:
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _configured_user_ips() -> list[str]:
|
|
258
|
+
"""Every non-backend entry configured anywhere, oldest source first:
|
|
259
|
+
``.env``, process env, then config.toml (the runtime / API-added one)."""
|
|
252
260
|
out: list[str] = []
|
|
253
|
-
for src in
|
|
261
|
+
for src in (_dotenv_value("AGENTOS_UI_ALLOWED_IPS"),
|
|
262
|
+
os.environ.get("AGENTOS_UI_ALLOWED_IPS", ""),
|
|
263
|
+
_load_raw().get("allowed_ips", "")):
|
|
254
264
|
for ip in _parse_ip_list(src):
|
|
255
|
-
if ip not in out:
|
|
265
|
+
if ip not in _DEFAULT_ALLOWED_IPS and ip not in out:
|
|
256
266
|
out.append(ip)
|
|
257
267
|
return out
|
|
258
268
|
|
|
259
269
|
|
|
270
|
+
def user_allowed_ip() -> str | None:
|
|
271
|
+
"""The ONE user IP allowed to reach the panel, or None.
|
|
272
|
+
|
|
273
|
+
The most recent wins: the runtime entry (config.toml — set by
|
|
274
|
+
``POST /api/ui/allowlist``) over ``.env``, and within a source the LAST entry,
|
|
275
|
+
since the documented way to add an IP is to append it. CIDR ranges are never
|
|
276
|
+
chosen — a range is many IPs.
|
|
277
|
+
"""
|
|
278
|
+
candidates = [ip for ip in _configured_user_ips() if _single_address(ip)]
|
|
279
|
+
return candidates[-1] if candidates else None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def ignored_allowed_ips() -> list[str]:
|
|
283
|
+
"""Configured user IPs/ranges that are NOT allowed, because only one may be."""
|
|
284
|
+
keep = user_allowed_ip()
|
|
285
|
+
return [ip for ip in _configured_user_ips() if ip != keep]
|
|
286
|
+
|
|
287
|
+
|
|
260
288
|
# Built-in baseline of tools a headless agent run may use. Kept here (not in
|
|
261
289
|
# local_exec) so it is one configurable setting rather than a literal buried in the
|
|
262
290
|
# spawn code: the set WILL grow, and a new tool must be addable without editing
|
|
@@ -434,23 +462,23 @@ def journal_enabled() -> bool:
|
|
|
434
462
|
|
|
435
463
|
|
|
436
464
|
def add_allowed_ip(ip: str) -> list[str]:
|
|
437
|
-
"""
|
|
465
|
+
"""Make ``ip`` THE user IP at runtime (persisted to config.toml ``allowed_ips``).
|
|
438
466
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
467
|
+
Only one user IP may reach the panel at a time, so this REPLACES whichever was
|
|
468
|
+
there before — including one from ``.env``, which it outranks while set.
|
|
469
|
+
Idempotent. The middleware sees it on the next request; the caller updates the
|
|
470
|
+
host firewall. A backend IP is already allowed and is left alone. Returns the
|
|
471
|
+
effective allowlist.
|
|
443
472
|
"""
|
|
444
473
|
ip = (ip or "").strip()
|
|
445
474
|
if not ip:
|
|
446
475
|
raise ValueError("ip is required")
|
|
447
|
-
if
|
|
476
|
+
if not _single_address(ip):
|
|
477
|
+
raise ValueError(f"{ip} is a range — only one IP may be allowed at a time")
|
|
478
|
+
if ip in _DEFAULT_ALLOWED_IPS:
|
|
448
479
|
return get_allowed_ips()
|
|
449
480
|
raw = _load_raw() # preserves token_enc / base_url / etc.
|
|
450
|
-
|
|
451
|
-
if ip not in entries:
|
|
452
|
-
entries.append(ip)
|
|
453
|
-
raw["allowed_ips"] = ", ".join(entries)
|
|
481
|
+
raw["allowed_ips"] = ip
|
|
454
482
|
_save_raw(raw)
|
|
455
483
|
return get_allowed_ips()
|
|
456
484
|
|
|
@@ -21,22 +21,17 @@ FAQS: list[tuple[str, str, str]] = [
|
|
|
21
21
|
("Basics", "How do I open the control panel from another machine?",
|
|
22
22
|
"**Easiest — an SSH tunnel (nothing to open):** on your machine run "
|
|
23
23
|
"`ssh -L 8765:127.0.0.1:8765 you@SERVER`, then browse `http://127.0.0.1:8765`.\n"
|
|
24
|
-
"**Directly by IP
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"`
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"`
|
|
34
|
-
"
|
|
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."),
|
|
24
|
+
"**Directly by IP** — only ONE outside IP can open the panel at a time, and only someone on the "
|
|
25
|
+
"server can set it:\n"
|
|
26
|
+
"1. On YOUR machine, find your public IP: `curl -s https://api.ipify.org; echo` → prints e.g. `203.0.113.7` "
|
|
27
|
+
"(Windows PowerShell: `Invoke-RestMethod https://api.ipify.org`)\n"
|
|
28
|
+
"2. On the SERVER (over SSH): `agentos allow-ip 203.0.113.7` — this REPLACES the previous IP, which loses access. "
|
|
29
|
+
"Or open the panel on the server itself (`http://127.0.0.1:8765`) → Dashboard → **Remote access**.\n"
|
|
30
|
+
"3. If it says the firewall needs root: `sudo agentos firewall --apply` (Windows: `agentos firewall --apply` "
|
|
31
|
+
"in an Administrator PowerShell).\n"
|
|
32
|
+
"4. Browse `http://SERVER-IP:8765` and paste your co_ token. `agentos allow-ip` shows the allowed IP; "
|
|
33
|
+
"`agentos allow-ip --clear` removes it. The remote API cannot change it, so a leaked token cannot "
|
|
34
|
+
"open the panel to another address."),
|
|
40
35
|
("Basics", "How do I use devops-bot-sdk on another machine?",
|
|
41
36
|
"Install it there too — the same checklist (Python, git, SSH key, repo access, claude or ad), then "
|
|
42
37
|
"`pip install \"git+ssh://git@github.com/consultancy-outfit/DevOpsBot-SDK.git\"` and `agentos configure` "
|
|
@@ -19,10 +19,12 @@ 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 with ``AGENTOS_FIREWALL_OPEN_ANY=1``.
|
|
26
28
|
|
|
27
29
|
Applying is deliberately opt-in: editing a live firewall is exactly the operation
|
|
28
30
|
that can lock an operator out of their own box, so the default is to print and let
|
|
@@ -30,6 +32,7 @@ a human read it first.
|
|
|
30
32
|
"""
|
|
31
33
|
from __future__ import annotations
|
|
32
34
|
|
|
35
|
+
import ipaddress
|
|
33
36
|
import os
|
|
34
37
|
import platform
|
|
35
38
|
import shutil
|
|
@@ -111,10 +114,13 @@ def allow_any_rules(port: int, system: str | None = None) -> list[str]:
|
|
|
111
114
|
corrected in seconds without root on the box, which is exactly what a
|
|
112
115
|
kernel rule cannot offer.
|
|
113
116
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
That reasoning held for *access* but not for *load*: every request from the
|
|
118
|
+
internet still costs a Python round-trip before the allowlist refuses it, and
|
|
119
|
+
a brute-force flood took panels to 100% CPU. So this is no longer the default
|
|
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
|
|
122
|
+
is still self-healing: the new IP goes in ``_DEFAULT_ALLOWED_IPS`` and the
|
|
123
|
+
next self-update adds its rule.
|
|
118
124
|
"""
|
|
119
125
|
sysname = (system or _system()).lower()
|
|
120
126
|
|
|
@@ -201,6 +207,17 @@ def ensure_open(port: int | None = None, *, apply: bool = True) -> tuple[bool, s
|
|
|
201
207
|
who = "Administrator" if sysname == "windows" else "root (sudo)"
|
|
202
208
|
return (False, f"TCP {port} is not open and opening it needs {who}:\n{printable}")
|
|
203
209
|
|
|
210
|
+
failed = _apply_rules(rules)
|
|
211
|
+
if failed:
|
|
212
|
+
return (False, "could not open TCP %d: %s" % (port, "; ".join(failed)))
|
|
213
|
+
return (True, f"opened TCP {port} inbound")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _apply_rules(rules: list[str]) -> list[str]:
|
|
217
|
+
"""Run each rule (already privileged, so a printed ``sudo `` is stripped).
|
|
218
|
+
|
|
219
|
+
Returns one ``"<cmd>: <reason>"`` per failure — empty means all applied.
|
|
220
|
+
"""
|
|
204
221
|
failed: list[str] = []
|
|
205
222
|
for rule in rules:
|
|
206
223
|
cmd = rule[len("sudo "):] if rule.startswith("sudo ") else rule
|
|
@@ -214,9 +231,130 @@ def ensure_open(port: int | None = None, *, apply: bool = True) -> tuple[bool, s
|
|
|
214
231
|
if proc.returncode != 0:
|
|
215
232
|
tail = (proc.stderr or proc.stdout or "").strip().splitlines()
|
|
216
233
|
failed.append(f"{cmd}: {tail[-1][:120] if tail else f'exit {proc.returncode}'}")
|
|
234
|
+
return failed
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _net(value: str):
|
|
238
|
+
"""``value`` as an ip_network, or None — so ``1.2.3.4`` and ``1.2.3.4/32`` match."""
|
|
239
|
+
try:
|
|
240
|
+
return ipaddress.ip_network(value.strip(), strict=False)
|
|
241
|
+
except ValueError:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def open_any_opted_in() -> bool:
|
|
246
|
+
"""Has the operator chosen to keep the port open to any source?
|
|
247
|
+
|
|
248
|
+
Only an explicit ``AGENTOS_FIREWALL_OPEN_ANY=1`` — read from the process env
|
|
249
|
+
or ``~/.agentos/.env``, like every other ``AGENTOS_UI_*`` toggle. (The removed
|
|
250
|
+
``AGENTOS_UI_PUBLIC=1`` used to imply it.)
|
|
251
|
+
"""
|
|
252
|
+
try:
|
|
253
|
+
from sdk.ui.ip_allowlist import _TRUE, _setting
|
|
254
|
+
except Exception: # noqa: BLE001 — never fail open on an import hiccup
|
|
255
|
+
return False
|
|
256
|
+
return _setting("AGENTOS_FIREWALL_OPEN_ANY").lower() in _TRUE
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def ensure_restricted(port: int | None = None, *, apply: bool = True,
|
|
260
|
+
ips: list[str] | None = None) -> tuple[bool, str]:
|
|
261
|
+
"""Limit ``port`` to EXACTLY the allowlisted IPs. Returns ``(changed, message)``.
|
|
262
|
+
Never raises.
|
|
263
|
+
|
|
264
|
+
Adds a per-IP rule for each allowlisted address that lacks one, THEN removes
|
|
265
|
+
the open-to-any rule and every per-IP rule for an address no longer allowed
|
|
266
|
+
(only one user IP may reach the panel, so replacing it must close the old
|
|
267
|
+
one) — in that order, and the removals are skipped if any add failed, so the
|
|
268
|
+
backend never loses its route mid-change. Same contract as
|
|
269
|
+
:func:`ensure_open` otherwise: unreadable state is never treated as closed,
|
|
270
|
+
an unprivileged run only returns the commands, and macOS never applies.
|
|
271
|
+
|
|
272
|
+
A message that needs the operator to act is multi-line (it carries commands),
|
|
273
|
+
which is what ``updater._ensure_firewall`` keys on to surface it.
|
|
274
|
+
"""
|
|
275
|
+
port = ui_port() if port is None else port
|
|
276
|
+
sysname = _system()
|
|
277
|
+
if ips is None:
|
|
278
|
+
try:
|
|
279
|
+
from sdk.config import get_allowed_ips
|
|
280
|
+
ips = get_allowed_ips()
|
|
281
|
+
except Exception: # noqa: BLE001
|
|
282
|
+
ips = []
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
sources = port_sources(port, sysname, collapse=False)
|
|
286
|
+
except Exception: # noqa: BLE001 — a status probe must never break an update
|
|
287
|
+
sources = None
|
|
288
|
+
|
|
289
|
+
inactive_note = ""
|
|
290
|
+
if sysname == "linux" and _ufw_inactive():
|
|
291
|
+
inactive_note = ("\nNOTE: ufw is INACTIVE, so no rule is enforced and the port is open "
|
|
292
|
+
"to everyone.\nEnable it once you have allowed SSH:\n"
|
|
293
|
+
" sudo ufw allow OpenSSH && sudo ufw enable")
|
|
294
|
+
|
|
295
|
+
if sources is None:
|
|
296
|
+
rules = allow_rules(ips, port, sysname) + [revoke_any_rule(port, sysname)]
|
|
297
|
+
printable = " " + "\n ".join(rules)
|
|
298
|
+
ok, tool = _tool_present()
|
|
299
|
+
why = tool if not ok else "needs root to read the firewall state"
|
|
300
|
+
return (False, f"could not read the firewall state ({why}) — to restrict TCP {port} "
|
|
301
|
+
f"to the allowlist yourself:\n{printable}")
|
|
302
|
+
|
|
303
|
+
have = {n for n in (_net(x) for x in sources) if n is not None}
|
|
304
|
+
want = {n for n in (_net(ip) for ip in ips) if n is not None}
|
|
305
|
+
missing = [ip for ip in ips if _net(ip) is not None and _net(ip) not in have]
|
|
306
|
+
stale = sorted(x for x in sources if _net(x) is not None and _net(x) not in want)
|
|
307
|
+
open_any = "Anywhere" in sources
|
|
308
|
+
|
|
309
|
+
if not missing and not open_any and not stale:
|
|
310
|
+
return (False, f"TCP {port} is restricted to the allowlist" + inactive_note)
|
|
311
|
+
|
|
312
|
+
removals = ([revoke_any_rule(port, sysname)] if open_any else []) + [
|
|
313
|
+
revoke_rule(x, port, sysname) for x in stale]
|
|
314
|
+
rules = allow_rules(missing, port, sysname) + removals
|
|
315
|
+
printable = " " + "\n ".join(rules)
|
|
316
|
+
what = "; ".join(w for w in (
|
|
317
|
+
f"TCP {port} is open to ANY source" if open_any else "",
|
|
318
|
+
f"missing a rule for: {', '.join(missing)}" if missing else "",
|
|
319
|
+
f"still allows: {', '.join(stale)}" if stale else "",
|
|
320
|
+
) if w)
|
|
321
|
+
|
|
322
|
+
if not apply or sysname == "darwin":
|
|
323
|
+
return (False, f"{what}. To restrict it to the allowlist:\n{printable}" + inactive_note)
|
|
324
|
+
if not _is_privileged():
|
|
325
|
+
who = "Administrator" if sysname == "windows" else "root (sudo)"
|
|
326
|
+
return (False, f"{what} and restricting it needs {who}:\n{printable}" + inactive_note)
|
|
327
|
+
|
|
328
|
+
failed = _apply_rules(allow_rules(missing, port, sysname))
|
|
217
329
|
if failed:
|
|
218
|
-
|
|
219
|
-
|
|
330
|
+
# The any-rule stays: removing it now could cut the backend off.
|
|
331
|
+
return (False, f"could not add allowlist rules for TCP {port}: {'; '.join(failed)}"
|
|
332
|
+
f"\nThe open-to-any rule was left in place.")
|
|
333
|
+
if removals:
|
|
334
|
+
_apply_rules(removals)
|
|
335
|
+
still = port_sources(port, sysname, collapse=False) or set()
|
|
336
|
+
left = sorted(x for x in still
|
|
337
|
+
if x == "Anywhere" or (_net(x) is not None and _net(x) not in want))
|
|
338
|
+
if left:
|
|
339
|
+
return (True, f"TCP {port} still allows {', '.join(left)} — a rule the SDK did "
|
|
340
|
+
f"not create. Find and delete it:\n "
|
|
341
|
+
+ ("sudo ufw status numbered" if sysname == "linux" else
|
|
342
|
+
"netsh advfirewall firewall show rule name=all dir=in")
|
|
343
|
+
+ inactive_note)
|
|
344
|
+
return (True, f"restricted TCP {port} to the allowlist: {', '.join(ips)}" + inactive_note)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def sync_rules(port: int | None = None) -> tuple[bool, str]:
|
|
348
|
+
"""Bring the host firewall in line with the allowlist after it changed.
|
|
349
|
+
|
|
350
|
+
Called when the one user IP is set or cleared (CLI or panel), so the new IP's
|
|
351
|
+
rule is added and the old one's removed straight away rather than at the next
|
|
352
|
+
self-update. Leaves a deliberately open port alone. Never raises.
|
|
353
|
+
"""
|
|
354
|
+
if open_any_opted_in():
|
|
355
|
+
return (False, "port is open to any source by choice (AGENTOS_FIREWALL_OPEN_ANY) — "
|
|
356
|
+
"no per-IP rules to change")
|
|
357
|
+
return ensure_restricted(port)
|
|
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,65 @@ 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
|
-
|
|
452
|
-
|
|
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
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def allow_ip_cli(argv: list[str]) -> int:
|
|
611
|
+
"""``agentos allow-ip [IP | --clear]`` — set, show or clear THE one user IP.
|
|
612
|
+
|
|
613
|
+
Only one outside IP may reach the panel at a time; setting a new one replaces
|
|
614
|
+
the old one in the allowlist and in the host firewall. This command is one of
|
|
615
|
+
the two ways to change it — both need someone on the machine (this one over
|
|
616
|
+
SSH, or the panel opened at http://127.0.0.1:8765 on the box); the remote API
|
|
617
|
+
refuses. Run it as the user the panel runs as, so it edits that user's
|
|
618
|
+
~/.agentos/config.toml; the firewall half needs root and prints the command
|
|
619
|
+
otherwise.
|
|
620
|
+
"""
|
|
621
|
+
from sdk import config
|
|
622
|
+
|
|
623
|
+
def show() -> None:
|
|
624
|
+
user = config.user_allowed_ip()
|
|
625
|
+
print(f"Allowed user IP : {user or '(none — only this machine and the backend)'}")
|
|
626
|
+
print(f"Backend IPs : {', '.join(config._DEFAULT_ALLOWED_IPS)}")
|
|
627
|
+
ignored = config.ignored_allowed_ips()
|
|
628
|
+
if ignored:
|
|
629
|
+
print(f"Ignored : {', '.join(ignored)} (only one user IP is allowed at a time)")
|
|
630
|
+
|
|
631
|
+
args = [a for a in argv if a.strip()]
|
|
632
|
+
if not args or args[0] in ("--show", "show"):
|
|
633
|
+
show()
|
|
634
|
+
return 0
|
|
635
|
+
|
|
636
|
+
if args[0] in ("--clear", "clear", "--remove"):
|
|
637
|
+
user = config.user_allowed_ip()
|
|
638
|
+
runtime = [ip for ip in config.runtime_allowed_ips() if ip == user]
|
|
639
|
+
if not runtime:
|
|
640
|
+
if user:
|
|
641
|
+
print(f"{user} comes from AGENTOS_UI_ALLOWED_IPS in ~/.agentos/.env — "
|
|
642
|
+
"remove it there.")
|
|
643
|
+
return 1
|
|
644
|
+
print("No user IP is allowed — nothing to clear.")
|
|
645
|
+
return 0
|
|
646
|
+
config.remove_allowed_ip(user)
|
|
647
|
+
print(f"✓ removed {user}")
|
|
648
|
+
else:
|
|
649
|
+
ip = args[0]
|
|
650
|
+
before = config.user_allowed_ip()
|
|
453
651
|
try:
|
|
454
|
-
|
|
455
|
-
except
|
|
456
|
-
print(f"
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
print(f"\n{len(rules) - failed}/{len(rules)} applied.")
|
|
468
|
-
return 1 if failed else 0
|
|
652
|
+
config.add_allowed_ip(ip)
|
|
653
|
+
except ValueError as exc:
|
|
654
|
+
print(f"error: {exc}")
|
|
655
|
+
return 2
|
|
656
|
+
print(f"✓ {ip} is now the one IP allowed to reach the panel")
|
|
657
|
+
if before and before != ip:
|
|
658
|
+
print(f" (replaced {before} — it no longer has access)")
|
|
659
|
+
|
|
660
|
+
changed, msg = sync_rules()
|
|
661
|
+
print(("↻ " if changed else "") + msg)
|
|
662
|
+
show()
|
|
663
|
+
return 0
|
|
469
664
|
|
|
470
665
|
|
|
471
666
|
if __name__ == "__main__": # pragma: no cover
|