devops-bot-sdk 1.7.45__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.45 → devops_bot_sdk-1.7.46}/PKG-INFO +1 -1
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/cli.py +18 -7
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/config.py +59 -31
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/faqs.py +11 -16
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/firewall.py +103 -47
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/help_data/areas.json +9 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/help_data/commands.json +11 -1
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/help_data/help.json +5 -1
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/help_data/pages.json +1 -1
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/help_data/releases.json +10 -0
- devops_bot_sdk-1.7.46/sdk/help_data/snapshot.json +17 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/helpdesk_kb.md +45 -33
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/swagger.py +0 -6
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/api.py +60 -25
- devops_bot_sdk-1.7.46/sdk/ui/ip_allowlist.py +199 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/models.py +8 -6
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/server.py +11 -8
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/app.js +62 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/index.html +22 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/styles.css +9 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/updater.py +1 -1
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_firewall_open_on_update.py +28 -12
- devops_bot_sdk-1.7.46/tests/test_one_allowed_ip.py +153 -0
- {devops_bot_sdk-1.7.45 → 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.45 → devops_bot_sdk-1.7.46}/tests/test_ui_gate.py +57 -73
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/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.46}/README.md +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/pyproject.toml +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/classifier.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/collectors/common.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/collectors/linux_auditd.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/collectors/macos_eslogger.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/collectors/windows_eventlog.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/models.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/os_setup.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/recorder.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/redact.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/settings.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/shipper.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/spool.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agent_sharing.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/browser_profile.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/claude_usage.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/client.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/custom_agents.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/deferred_tickets.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/estimator.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/fallback_handoff.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/github_fetch.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/help_actions.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/helpdesk.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/instructions_template.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/journal.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/log_colors.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/memory_guard.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/model_policy.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/prd_progress.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/providers.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/resume.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/router.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/schedule_tickets.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/schedule_timeline.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/scope_plan.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/self_report.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/self_report_local.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/task_types.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/test.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/test_env.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ticket_board.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ticket_bulk.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ticket_plan.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ticket_pools.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/tickets.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/tiers/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/tiers/mini_agent.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/faqs.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/help.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/swagger.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/url_access.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/vps_identity.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/write_hook.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/setup.cfg +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_activity_claude_runs.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_activity_collectors.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_activity_core.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_activity_live_tracking.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_activity_macos.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_activity_os_setup.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_activity_pipeline.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_crud_cli.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_sharing.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_approval_park.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_auto_ticket_queue.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_browser_profile.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_cheap_tier.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_claude_usage_gate.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_crash_recovery.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_create_safe_retry.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_deferred_tickets.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_done_transition.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_estimator.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_fallback_cli.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_fallback_cli_upgrade.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_fallback_rate_limit.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_faqs.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_github_fetch.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_graph_off_by_default.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_help_actions.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_help_attachments.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_help_questions.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_helpdesk.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_infographic_data.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_instructions_template.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_intake_job_outcome.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_journal.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_limit_fallback_handoff.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_local_exec_provider.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_log_colors_backlog.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_mcp_route_contract.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_memory_guard.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_model_policy.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_panel_help.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_panel_ticket_schedules.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_parallel_tickets.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_plan_ahead.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_pool_pr_modes.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_pr_identity.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_prd_artifact.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_project_path_resolution.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_providers.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_quiet_run_log.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_resume.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_router.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_run_verdict_is_honest.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_schedule_outputs_and_plans.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_schedule_tickets.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_schedule_timeline.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_scheduler_transient_retry.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_self_report.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_stale_panel_restart.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_theme_switch.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_board.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_bulk.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_cli.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_detail_view.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_plan.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_pools.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_schedule_and_agent.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_tier_flag_parity.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ui_no_native_dialogs.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_updater_ignores_cwd.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_updater_pep668.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_updater_reports_the_truth.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_url_access.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_version_flag.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_vps_health.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_vps_only_reporting.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_windows_loop_identity.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_windows_panel_task.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_windows_stop_and_pidfile.py +0 -0
- {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_windows_task_paths.py +0 -0
- {devops_bot_sdk-1.7.45 → 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
|
|
@@ -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` "
|
|
@@ -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
|
"""
|
|
@@ -246,24 +245,27 @@ def _net(value: str):
|
|
|
246
245
|
def open_any_opted_in() -> bool:
|
|
247
246
|
"""Has the operator chosen to keep the port open to any source?
|
|
248
247
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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.)
|
|
252
251
|
"""
|
|
253
252
|
try:
|
|
254
|
-
from sdk.ui.ip_allowlist import _TRUE, _setting
|
|
253
|
+
from sdk.ui.ip_allowlist import _TRUE, _setting
|
|
255
254
|
except Exception: # noqa: BLE001 — never fail open on an import hiccup
|
|
256
255
|
return False
|
|
257
|
-
return
|
|
256
|
+
return _setting("AGENTOS_FIREWALL_OPEN_ANY").lower() in _TRUE
|
|
258
257
|
|
|
259
258
|
|
|
260
259
|
def ensure_restricted(port: int | None = None, *, apply: bool = True,
|
|
261
260
|
ips: list[str] | None = None) -> tuple[bool, str]:
|
|
262
|
-
"""Limit ``port`` to the allowlisted IPs. Returns ``(changed, message)``.
|
|
261
|
+
"""Limit ``port`` to EXACTLY the allowlisted IPs. Returns ``(changed, message)``.
|
|
262
|
+
Never raises.
|
|
263
263
|
|
|
264
264
|
Adds a per-IP rule for each allowlisted address that lacks one, THEN removes
|
|
265
|
-
the open-to-any rule
|
|
266
|
-
|
|
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
|
|
267
269
|
:func:`ensure_open` otherwise: unreadable state is never treated as closed,
|
|
268
270
|
an unprivileged run only returns the commands, and macOS never applies.
|
|
269
271
|
|
|
@@ -299,18 +301,23 @@ def ensure_restricted(port: int | None = None, *, apply: bool = True,
|
|
|
299
301
|
f"to the allowlist yourself:\n{printable}")
|
|
300
302
|
|
|
301
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}
|
|
302
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)
|
|
303
307
|
open_any = "Anywhere" in sources
|
|
304
308
|
|
|
305
|
-
if not missing and not open_any:
|
|
309
|
+
if not missing and not open_any and not stale:
|
|
306
310
|
return (False, f"TCP {port} is restricted to the allowlist" + inactive_note)
|
|
307
311
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
311
315
|
printable = " " + "\n ".join(rules)
|
|
312
|
-
what =
|
|
313
|
-
|
|
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)
|
|
314
321
|
|
|
315
322
|
if not apply or sysname == "darwin":
|
|
316
323
|
return (False, f"{what}. To restrict it to the allowlist:\n{printable}" + inactive_note)
|
|
@@ -323,38 +330,31 @@ def ensure_restricted(port: int | None = None, *, apply: bool = True,
|
|
|
323
330
|
# The any-rule stays: removing it now could cut the backend off.
|
|
324
331
|
return (False, f"could not add allowlist rules for TCP {port}: {'; '.join(failed)}"
|
|
325
332
|
f"\nThe open-to-any rule was left in place.")
|
|
326
|
-
if
|
|
327
|
-
|
|
333
|
+
if removals:
|
|
334
|
+
_apply_rules(removals)
|
|
328
335
|
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
|
-
|
|
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.
|
|
346
353
|
"""
|
|
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
354
|
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}")
|
|
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)
|
|
358
358
|
|
|
359
359
|
|
|
360
360
|
def revoke_rule(ip: str, port: int, system: str | None = None) -> str:
|
|
@@ -607,5 +607,61 @@ def cli(argv: list[str]) -> int:
|
|
|
607
607
|
return 0 if changed or "restricted to the allowlist" in msg else 1
|
|
608
608
|
|
|
609
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()
|
|
651
|
+
try:
|
|
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
|
|
664
|
+
|
|
665
|
+
|
|
610
666
|
if __name__ == "__main__": # pragma: no cover
|
|
611
667
|
sys.exit(cli(sys.argv[1:]))
|
|
@@ -80,6 +80,15 @@
|
|
|
80
80
|
"latest_pr": 143,
|
|
81
81
|
"latest_date": "2026-10-16"
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
"area": "General",
|
|
85
|
+
"changes_30d": 1,
|
|
86
|
+
"features_30d": 0,
|
|
87
|
+
"fixes_30d": 0,
|
|
88
|
+
"latest": "security: remove public mode; ONE outside IP at a time, set only on the server (allow-ip + Remote access)",
|
|
89
|
+
"latest_pr": 189,
|
|
90
|
+
"latest_date": "2026-10-30"
|
|
91
|
+
},
|
|
83
92
|
{
|
|
84
93
|
"area": "Local Exec",
|
|
85
94
|
"changes_30d": 1,
|
|
@@ -509,12 +509,22 @@
|
|
|
509
509
|
"doc": "https://github.com/consultancy-outfit/DevOpsBot-SDK/blob/main/docs/COMMANDS.md",
|
|
510
510
|
"id": "agentos doctor"
|
|
511
511
|
},
|
|
512
|
+
{
|
|
513
|
+
"command": "agentos allow-ip",
|
|
514
|
+
"syntax": "agentos allow-ip [IP | --clear]",
|
|
515
|
+
"program": "agentos",
|
|
516
|
+
"area": "Other",
|
|
517
|
+
"description": "the ONE outside IP allowed to reach the panel (on the server only)",
|
|
518
|
+
"valid": true,
|
|
519
|
+
"doc": "https://github.com/consultancy-outfit/DevOpsBot-SDK/blob/main/docs/COMMANDS.md",
|
|
520
|
+
"id": "agentos allow-ip"
|
|
521
|
+
},
|
|
512
522
|
{
|
|
513
523
|
"command": "agentos firewall",
|
|
514
524
|
"syntax": "agentos firewall [--apply] [--any] [--revoke IP] [--port N]",
|
|
515
525
|
"program": "agentos",
|
|
516
526
|
"area": "Admin",
|
|
517
|
-
"description": "
|
|
527
|
+
"description": "limit the control panel's port to the allowlisted IPs",
|
|
518
528
|
"valid": true,
|
|
519
529
|
"doc": "https://github.com/consultancy-outfit/DevOpsBot-SDK/blob/main/docs/COMMANDS.md",
|
|
520
530
|
"id": "agentos firewall"
|
|
@@ -57,6 +57,10 @@
|
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
"issue": "After `agentos-auto update`: \"no systemd unit 'agentos-ui' — restart the panel manually\", \"could not read the firewall state (needs root …) … sudo ufw allow 8765/tcp\", \"VPS health: not reported — not_assigned_vps\"",
|
|
60
|
-
"fix": "Not errors. The update restarts the panel so it runs the new code (`sdk/ui/service.py`),
|
|
60
|
+
"fix": "Not errors. The update restarts the panel so it runs the new code (`sdk/ui/service.py`), limits the panel's port to the allowed IPs — the backend and the one outside IP; it removes any open-to-everyone rule (`sdk/firewall.py` `ensure_restricted` — it never runs sudo itself, so unprivileged it prints the commands instead), and reports VPS health only from the VPS assigned to the account (`sdk/vps_health.py`). On a laptop or any machine that is not the assigned VPS all three are expected and need no action; on the VPS, run `sudo agentos firewall --apply`, and restart `agentos-ui` if it runs by hand. \"ufw is INACTIVE\" in that output means no rule is enforced at all: `sudo ufw allow OpenSSH && sudo ufw enable`."
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"issue": "Panel at 100% CPU / slow, lots of 403s or unknown IPs in `ss -tn sport = :8765`",
|
|
64
|
+
"fix": "Internet scanners reaching an open 8765. On 1.7.45+ refusals are cheap and capped, but the fix is the firewall: `sudo agentos firewall --apply`, then `sudo ufw status verbose` (8765 → only the allowed IPs; Status active). Older SDKs re-opened the port to everyone on every update — `agentos-auto update` first."
|
|
61
65
|
}
|
|
62
66
|
]
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"page": "Dashboard",
|
|
4
4
|
"file": "index.html",
|
|
5
5
|
"heading": "Daily ticket activity",
|
|
6
|
-
"summary": ""
|
|
6
|
+
"summary": "Bot — waiting for work To Do— In Progress— Done— Backlog— Daily ticket activity 7 days 30 days To Do In Progress Done Backlog Up next0 Remote access Allowed IP: —"
|
|
7
7
|
},
|
|
8
8
|
{
|
|
9
9
|
"page": "Run & Logs",
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"pr": 189,
|
|
4
|
+
"date": "2026-10-30",
|
|
5
|
+
"merged_at": "2026-10-30T14:14:39+05:00",
|
|
6
|
+
"type": "Other",
|
|
7
|
+
"area": "General",
|
|
8
|
+
"title": "security: remove public mode; ONE outside IP at a time, set only on the server (allow-ip + Remote access)",
|
|
9
|
+
"version": "1.7.46",
|
|
10
|
+
"url": "https://github.com/consultancy-outfit/DevOpsBot-SDK/pull/189"
|
|
11
|
+
},
|
|
2
12
|
{
|
|
3
13
|
"pr": 188,
|
|
4
14
|
"date": "2026-10-29",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.7.46",
|
|
4
|
+
"commit": "5f00536",
|
|
5
|
+
"pushed_at": "2026-10-30T14:14:39+05:00",
|
|
6
|
+
"generated_at": "2026-10-30T09:15:50+00:00",
|
|
7
|
+
"commands": 59,
|
|
8
|
+
"commands_invalid": 0,
|
|
9
|
+
"commands_undocumented": 0,
|
|
10
|
+
"panel_pages": 11,
|
|
11
|
+
"tests": 2597,
|
|
12
|
+
"prs_7d": 25,
|
|
13
|
+
"prs_30d": 41,
|
|
14
|
+
"features_30d": 12,
|
|
15
|
+
"fixes_30d": 18
|
|
16
|
+
}
|
|
17
|
+
]
|