devops-bot-sdk 1.6.69__tar.gz → 1.6.71__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/SOURCES.txt +3 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/os_setup.py +5 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/settings.py +21 -1
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/shipper.py +75 -3
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/spool.py +51 -16
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/activity.py +1 -1
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/client.py +39 -8
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/vps_health.py +9 -1
- devops_bot_sdk-1.6.71/sdk/vps_identity.py +174 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_activity_collectors.py +8 -1
- devops_bot_sdk-1.6.71/tests/test_activity_live_tracking.py +145 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_activity_macos.py +8 -1
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_activity_pipeline.py +18 -14
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_vps_health.py +9 -0
- devops_bot_sdk-1.6.71/tests/test_vps_only_reporting.py +234 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/README.md +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/classifier.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/collectors/common.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/collectors/linux_auditd.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/collectors/macos_eslogger.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/collectors/windows_eventlog.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/models.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/recorder.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/redact.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/browser_profile.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/config.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/custom_agents.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/estimator.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/firewall.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/github_fetch.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/journal.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/memory_guard.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/prd_progress.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/providers.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/resume.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/router.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/schedule_tickets.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/self_report.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/self_report_local.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/swagger.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/task_types.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/tickets.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/tiers/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/tiers/mini_agent.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/api.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/swagger.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/url_access.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/vendor/aidraftsman-cli-0.1.0.tgz +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/write_hook.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_activity_core.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_activity_os_setup.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_crud_cli.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_browser_profile.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_cheap_tier.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_crash_recovery.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_done_transition.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_estimator.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_fallback_cli.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_firewall_open_on_update.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_github_fetch.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_intake_job_outcome.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_journal.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_local_exec_provider.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_mcp_route_contract.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_memory_guard.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_pr_identity.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_prd_artifact.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_project_path_resolution.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_providers.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_resume.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_router.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_run_verdict_is_honest.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_schedule_outputs_and_plans.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_schedule_tickets.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_scheduler_transient_retry.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_self_report.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_stale_panel_restart.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_swagger_command.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_ticket_cli.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_tier_flag_parity.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_ui_gate.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_ui_lifespan.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_updater_pep668.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_updater_reports_the_truth.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_url_access.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_windows_loop_identity.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_windows_task_paths.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_write_hook.py +0 -0
|
@@ -53,6 +53,7 @@ sdk/tickets.py
|
|
|
53
53
|
sdk/updater.py
|
|
54
54
|
sdk/url_access.py
|
|
55
55
|
sdk/vps_health.py
|
|
56
|
+
sdk/vps_identity.py
|
|
56
57
|
sdk/write_hook.py
|
|
57
58
|
sdk/activity/__init__.py
|
|
58
59
|
sdk/activity/classifier.py
|
|
@@ -143,6 +144,7 @@ sdk/ui/static/tickets.html
|
|
|
143
144
|
sdk/vendor/aidraftsman-cli-0.1.0.tgz
|
|
144
145
|
tests/test_activity_collectors.py
|
|
145
146
|
tests/test_activity_core.py
|
|
147
|
+
tests/test_activity_live_tracking.py
|
|
146
148
|
tests/test_activity_macos.py
|
|
147
149
|
tests/test_activity_os_setup.py
|
|
148
150
|
tests/test_activity_pipeline.py
|
|
@@ -226,6 +228,7 @@ tests/test_updater_pep668.py
|
|
|
226
228
|
tests/test_updater_reports_the_truth.py
|
|
227
229
|
tests/test_url_access.py
|
|
228
230
|
tests/test_vps_health.py
|
|
231
|
+
tests/test_vps_only_reporting.py
|
|
229
232
|
tests/test_windows_loop_identity.py
|
|
230
233
|
tests/test_windows_task_paths.py
|
|
231
234
|
tests/test_write_hook.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
|
|
2
2
|
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.71
|
|
4
4
|
|
|
5
5
|
Public surface:
|
|
6
6
|
BackendClient.from_config() — create client from ~/.agentos/config.toml
|
|
@@ -34,7 +34,7 @@ Rules:
|
|
|
34
34
|
- All data egress through submit_webhook only
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
-
__version__ = "1.6.
|
|
37
|
+
__version__ = "1.6.71" # 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
|
|
@@ -188,6 +188,11 @@ def _flag_value() -> str:
|
|
|
188
188
|
return value.strip().strip('"').strip("'")
|
|
189
189
|
except OSError:
|
|
190
190
|
pass
|
|
191
|
+
# Under sudo, settings.setting reads ROOT's .env. Falling back to it made a
|
|
192
|
+
# flag left in /root/.agentos/.env answer for the panel user, so _enable_flag
|
|
193
|
+
# never wrote theirs and the collector never started — on every restart.
|
|
194
|
+
if _is_privileged() and _panel_user() is not None:
|
|
195
|
+
return ""
|
|
191
196
|
return settings.setting(AUDIT_FLAG)
|
|
192
197
|
|
|
193
198
|
|
|
@@ -23,8 +23,12 @@ from sdk.activity.spool import Spool
|
|
|
23
23
|
|
|
24
24
|
logger = logging.getLogger("sdk.activity.settings")
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
# Live tracking: one POST a minute is far under the relay's 60/min budget, and the
|
|
27
|
+
# five-minute default meant the newest activity was always up to five minutes stale.
|
|
28
|
+
DEFAULT_SHIP_INTERVAL_S = 60.0
|
|
27
29
|
MIN_SHIP_INTERVAL_S = 30.0
|
|
30
|
+
#: Events older than this are discarded instead of shipped (``0`` keeps everything).
|
|
31
|
+
DEFAULT_MAX_AGE_S = 3600.0
|
|
28
32
|
_FALSEY = {"0", "false", "no", "off"}
|
|
29
33
|
_TRUTHY = {"1", "true", "yes", "on"}
|
|
30
34
|
|
|
@@ -63,6 +67,22 @@ def ship_interval_seconds() -> float:
|
|
|
63
67
|
return DEFAULT_SHIP_INTERVAL_S
|
|
64
68
|
|
|
65
69
|
|
|
70
|
+
def max_age_seconds() -> float:
|
|
71
|
+
"""``AGENTOS_ACTIVITY_MAX_AGE_S`` — how old an event may be and still ship.
|
|
72
|
+
|
|
73
|
+
Default one hour: the trail is for live tracking, and a backlog shipped
|
|
74
|
+
oldest-first kept busy hosts days behind. ``0`` disables the cut-off.
|
|
75
|
+
"""
|
|
76
|
+
raw = setting("AGENTOS_ACTIVITY_MAX_AGE_S")
|
|
77
|
+
if not raw:
|
|
78
|
+
return DEFAULT_MAX_AGE_S
|
|
79
|
+
try:
|
|
80
|
+
return max(0.0, float(raw))
|
|
81
|
+
except ValueError:
|
|
82
|
+
logger.warning("AGENTOS_ACTIVITY_MAX_AGE_S=%r is not a number; using default", raw)
|
|
83
|
+
return DEFAULT_MAX_AGE_S
|
|
84
|
+
|
|
85
|
+
|
|
66
86
|
def spool_path() -> Path:
|
|
67
87
|
"""``~/.agentos/activity.db`` — resolved at call time so tests can redirect HOME."""
|
|
68
88
|
return Path.home() / ".agentos" / "activity.db"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Ship the activity spool to devops-bot-ai every
|
|
1
|
+
"""Ship the activity spool to devops-bot-ai every minute, newest first.
|
|
2
2
|
|
|
3
3
|
Same shape as ``sdk.vps_health``: an asyncio loop with a ``start()`` the UI
|
|
4
4
|
server's lifespan calls, cancelled by the host on shutdown.
|
|
@@ -25,8 +25,10 @@ from __future__ import annotations
|
|
|
25
25
|
|
|
26
26
|
import asyncio
|
|
27
27
|
import logging
|
|
28
|
+
import time
|
|
28
29
|
from collections.abc import Awaitable, Callable
|
|
29
30
|
from dataclasses import dataclass
|
|
31
|
+
from datetime import datetime, timezone
|
|
30
32
|
from typing import Any, Protocol
|
|
31
33
|
|
|
32
34
|
from sdk.activity import settings
|
|
@@ -176,12 +178,82 @@ async def flush_once(
|
|
|
176
178
|
return result
|
|
177
179
|
|
|
178
180
|
|
|
181
|
+
def discard_spool(spool: Spool | None = None) -> int:
|
|
182
|
+
"""Delete everything queued, without sending it. Returns the rows dropped."""
|
|
183
|
+
spool = spool or settings.get_spool()
|
|
184
|
+
dropped = 0
|
|
185
|
+
while True:
|
|
186
|
+
chunk = spool.next_chunk()
|
|
187
|
+
if not chunk:
|
|
188
|
+
return dropped
|
|
189
|
+
dropped += len(chunk)
|
|
190
|
+
spool.ack(chunk)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def purge_stale_rows(spool: Spool | None = None, now: float | None = None) -> int:
|
|
194
|
+
"""Discard what is older than ``settings.max_age_seconds()``. Returns rows removed."""
|
|
195
|
+
max_age = settings.max_age_seconds()
|
|
196
|
+
if max_age <= 0:
|
|
197
|
+
return 0
|
|
198
|
+
spool = spool or settings.get_spool()
|
|
199
|
+
cutoff = (now if now is not None else time.time()) - max_age
|
|
200
|
+
cutoff_iso = (datetime.fromtimestamp(cutoff, timezone.utc)
|
|
201
|
+
.isoformat(timespec="milliseconds").replace("+00:00", "Z"))
|
|
202
|
+
dropped = spool.purge_stale(cutoff_iso)
|
|
203
|
+
if dropped:
|
|
204
|
+
logger.info("activity.stale_discarded rows=%d older_than_s=%.0f "
|
|
205
|
+
"(live tracking: only recent activity is shipped)", dropped, max_age)
|
|
206
|
+
return dropped
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
async def ship_tick(
|
|
210
|
+
is_vps: Callable[[], Awaitable[bool | None]] | None = None,
|
|
211
|
+
) -> FlushResult | None:
|
|
212
|
+
"""One cron tick: ship only from the caller's assigned VPS, newest first.
|
|
213
|
+
|
|
214
|
+
The relay files every batch under the TOKEN's VPS, so a laptop running the SDK
|
|
215
|
+
with the same token put its own events in the VPS's audit trail. On a machine
|
|
216
|
+
that is definitely not the VPS the queue is discarded — kept, it would only grow
|
|
217
|
+
forever, since that machine will never be allowed to send it. When the answer
|
|
218
|
+
is unknown (profile unreachable) everything recent is kept for the next tick.
|
|
219
|
+
|
|
220
|
+
Every tick first discards events older than ``AGENTOS_ACTIVITY_MAX_AGE_S``
|
|
221
|
+
(default one hour): the trail is for live tracking, and a backlog — a host
|
|
222
|
+
that was offline, or flooded by a brute-force attack — must never hold the
|
|
223
|
+
current activity back.
|
|
224
|
+
"""
|
|
225
|
+
try:
|
|
226
|
+
purge_stale_rows()
|
|
227
|
+
except Exception as exc: # noqa: BLE001 — a purge failure must not stop shipping
|
|
228
|
+
logger.warning("activity: stale purge failed: %s", exc)
|
|
229
|
+
if is_vps is None:
|
|
230
|
+
from sdk import vps_identity
|
|
231
|
+
is_vps = vps_identity.is_assigned_vps
|
|
232
|
+
try:
|
|
233
|
+
verdict = await is_vps()
|
|
234
|
+
except Exception: # noqa: BLE001 — is_assigned_vps never raises, but be certain
|
|
235
|
+
verdict = None
|
|
236
|
+
if verdict is None:
|
|
237
|
+
return None
|
|
238
|
+
if verdict is False:
|
|
239
|
+
try:
|
|
240
|
+
dropped = discard_spool()
|
|
241
|
+
except Exception as exc: # noqa: BLE001
|
|
242
|
+
logger.warning("activity: could not discard the spool: %s", exc)
|
|
243
|
+
return None
|
|
244
|
+
if dropped:
|
|
245
|
+
logger.info("activity.not_assigned_vps discarded=%d (this machine is not the "
|
|
246
|
+
"user's VPS; nothing is shipped from it)", dropped)
|
|
247
|
+
return None
|
|
248
|
+
return await flush_once()
|
|
249
|
+
|
|
250
|
+
|
|
179
251
|
async def run_forever(interval_s: float | None = None, *, initial_delay_s: float = 30.0) -> None:
|
|
180
|
-
"""Flush every ``interval_s`` (default
|
|
252
|
+
"""Flush every ``interval_s`` (default 60 s) until cancelled."""
|
|
181
253
|
await asyncio.sleep(initial_delay_s)
|
|
182
254
|
while True:
|
|
183
255
|
try:
|
|
184
|
-
await
|
|
256
|
+
await ship_tick()
|
|
185
257
|
every = interval_s if interval_s is not None else settings.ship_interval_seconds()
|
|
186
258
|
await asyncio.sleep(every)
|
|
187
259
|
except asyncio.CancelledError:
|
|
@@ -169,33 +169,68 @@ class Spool:
|
|
|
169
169
|
max_sessions: int = MAX_SESSIONS_PER_CHUNK,
|
|
170
170
|
max_bytes: int = MAX_CHUNK_BYTES,
|
|
171
171
|
) -> Chunk:
|
|
172
|
-
"""
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
172
|
+
"""Queued sessions, then the NEWEST events, within the per-request limits.
|
|
173
|
+
|
|
174
|
+
Live tracking needs the latest activity first. Shipping oldest-first left
|
|
175
|
+
busy hosts (a brute-forced RDP port alone is thousands of events an hour)
|
|
176
|
+
days behind, so the dashboard showed Tuesday's events on Thursday. Sessions
|
|
177
|
+
still go first, in arrival order: they are few, and an event must never
|
|
178
|
+
reach the relay ahead of the session it references. Does NOT remove
|
|
179
|
+
anything — call :meth:`ack` after delivery.
|
|
177
180
|
"""
|
|
178
181
|
chunk = Chunk()
|
|
179
|
-
|
|
182
|
+
size = 0
|
|
180
183
|
with self._lock:
|
|
181
|
-
|
|
182
|
-
"SELECT seq, rev, kind, payload FROM spool
|
|
183
|
-
|
|
184
|
+
sessions = self._db.execute(
|
|
185
|
+
"SELECT seq, rev, kind, payload FROM spool WHERE kind = 'session' "
|
|
186
|
+
"ORDER BY seq LIMIT ?",
|
|
187
|
+
(max_sessions,),
|
|
184
188
|
).fetchall()
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
events = self._db.execute(
|
|
190
|
+
"SELECT seq, rev, kind, payload FROM spool WHERE kind = 'event' "
|
|
191
|
+
"ORDER BY seq DESC LIMIT ?",
|
|
192
|
+
(max_events,),
|
|
193
|
+
).fetchall()
|
|
194
|
+
for seq, rev, kind, payload in [*sessions, *events]:
|
|
190
195
|
if chunk.rows and size + len(payload) > max_bytes:
|
|
191
196
|
break
|
|
192
197
|
# Re-fit on the way out too: rows queued by an older SDK predate the clamp.
|
|
193
198
|
chunk.rows.append(Row(seq, rev, kind, fit_wire_limits(kind, json.loads(payload))))
|
|
194
|
-
n_events += kind == "event"
|
|
195
|
-
n_sessions += kind == "session"
|
|
196
199
|
size += len(payload)
|
|
197
200
|
return chunk
|
|
198
201
|
|
|
202
|
+
def purge_stale(self, cutoff_iso: str) -> int:
|
|
203
|
+
"""Delete events that happened before ``cutoff_iso``, and sessions that ENDED
|
|
204
|
+
before it. Returns the rows removed.
|
|
205
|
+
|
|
206
|
+
For live tracking an event an hour old is history, and shipping it only
|
|
207
|
+
delays the current ones. Open sessions are kept whatever their age — a
|
|
208
|
+
login that is still going on is live state. ``cutoff_iso`` uses the wire
|
|
209
|
+
format (``2026-10-22T07:06:01.971Z``), so string order is time order.
|
|
210
|
+
"""
|
|
211
|
+
with self._lock:
|
|
212
|
+
try:
|
|
213
|
+
cur = self._db.execute(
|
|
214
|
+
"DELETE FROM spool WHERE "
|
|
215
|
+
"(kind = 'event' AND json_extract(payload, '$.occurredAt') < ?) OR "
|
|
216
|
+
"(kind = 'session' AND json_extract(payload, '$.endedAt') IS NOT NULL "
|
|
217
|
+
" AND json_extract(payload, '$.endedAt') < ?)",
|
|
218
|
+
(cutoff_iso, cutoff_iso),
|
|
219
|
+
)
|
|
220
|
+
return int(cur.rowcount or 0)
|
|
221
|
+
except sqlite3.OperationalError:
|
|
222
|
+
# SQLite built without JSON1: filter in Python instead.
|
|
223
|
+
stale = []
|
|
224
|
+
for seq, kind, payload in self._db.execute(
|
|
225
|
+
"SELECT seq, kind, payload FROM spool"
|
|
226
|
+
).fetchall():
|
|
227
|
+
wire = json.loads(payload)
|
|
228
|
+
when = wire.get("occurredAt") if kind == "event" else wire.get("endedAt")
|
|
229
|
+
if when and str(when) < cutoff_iso:
|
|
230
|
+
stale.append((seq,))
|
|
231
|
+
self._db.executemany("DELETE FROM spool WHERE seq = ?", stale)
|
|
232
|
+
return len(stale)
|
|
233
|
+
|
|
199
234
|
# ── ack ──────────────────────────────────────────────────────────────────
|
|
200
235
|
|
|
201
236
|
def ack(self, chunk: Chunk) -> None:
|
|
@@ -79,7 +79,7 @@ def describe(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
|
|
|
79
79
|
# per-run event. So an agent run costs one POST no matter how chatty it is.
|
|
80
80
|
_LINE_BUF: dict[str, list[str]] = {}
|
|
81
81
|
_LINE_BUF_MAX_LINES = 40 # keep the tail — recent lines are the useful ones
|
|
82
|
-
_LINE_BUF_MAX_CHARS = 4000 #
|
|
82
|
+
_LINE_BUF_MAX_CHARS = 4000 # trimmed to the gateway limit by client._fit_activity
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
def note(bucket: str, text: str) -> None:
|
|
@@ -121,9 +121,19 @@ def _notify_status(task_id: str, status: str) -> None:
|
|
|
121
121
|
# `logger.info` line regardless of whether it buffers or POSTs.
|
|
122
122
|
_ACTIVITY_FLUSH_MIN_S = float(os.getenv("AGENTOS_ACTIVITY_FLUSH_MIN_S", "") or 180.0)
|
|
123
123
|
_ACTIVITY_FLUSH_MAX_S = float(os.getenv("AGENTOS_ACTIVITY_FLUSH_MAX_S", "") or 300.0)
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
|
|
124
|
+
# The gateway rejects an `activity` over 2000 characters with a 400 ("Activity
|
|
125
|
+
# must be shorter than or equal to 2000 characters"). The cap used to be 8000, so
|
|
126
|
+
# every coalesced flush and every long scheduled-run summary was refused and the
|
|
127
|
+
# run left no trace in agent_activities. Keep the tail: the most recent lines are
|
|
128
|
+
# the most useful.
|
|
129
|
+
_ACTIVITY_MAX_CHARS = 2000
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _fit_activity(text: str) -> str:
|
|
133
|
+
"""``text`` cut to the gateway's limit, keeping the tail behind a ``…`` marker."""
|
|
134
|
+
if len(text) <= _ACTIVITY_MAX_CHARS:
|
|
135
|
+
return text
|
|
136
|
+
return "…\n" + text[-(_ACTIVITY_MAX_CHARS - 2):]
|
|
127
137
|
|
|
128
138
|
|
|
129
139
|
class ActivityFlusher:
|
|
@@ -167,9 +177,7 @@ class ActivityFlusher:
|
|
|
167
177
|
if not self._buf:
|
|
168
178
|
return # nothing pending → don't open a connection at all
|
|
169
179
|
lines, self._buf = self._buf, [] # swap out atomically (no await between)
|
|
170
|
-
body = "\n".join(lines)
|
|
171
|
-
if len(body) > _ACTIVITY_FLUSH_MAX_CHARS:
|
|
172
|
-
body = "…\n" + body[-_ACTIVITY_FLUSH_MAX_CHARS:]
|
|
180
|
+
body = _fit_activity("\n".join(lines))
|
|
173
181
|
try:
|
|
174
182
|
await self._client.log_activity(body, task_id=self._task_id, immediate=True)
|
|
175
183
|
except Exception as exc: # noqa: BLE001 — never let a flush break a run
|
|
@@ -313,6 +321,29 @@ class BackendClient:
|
|
|
313
321
|
except Exception as exc:
|
|
314
322
|
raise BackendUnreachable(self._base_url, str(exc)) from exc
|
|
315
323
|
|
|
324
|
+
async def assigned_vps_ip(self) -> str:
|
|
325
|
+
"""The IP of the caller's assigned VPS from ``GET /ml-api/users/profile``.
|
|
326
|
+
|
|
327
|
+
``""`` when the profile names no VPS. Raises BackendUnreachable /
|
|
328
|
+
BackendAuthFailed — the caller (``sdk.vps_identity``) turns those into
|
|
329
|
+
"unknown" rather than "not a VPS".
|
|
330
|
+
"""
|
|
331
|
+
from sdk.config import BE_DEFAULT_URL
|
|
332
|
+
from sdk.vps_identity import extract_vps_ip
|
|
333
|
+
|
|
334
|
+
url = f"{BE_DEFAULT_URL.rstrip('/')}/ml-api/users/profile"
|
|
335
|
+
headers = {**self._headers, "accept": "*/*", "developer-name": "null", "product": ""}
|
|
336
|
+
try:
|
|
337
|
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
338
|
+
resp = await client.get(url, headers=headers)
|
|
339
|
+
_check_status(resp.status_code, url)
|
|
340
|
+
data = (resp.json() or {}).get("data") or {}
|
|
341
|
+
except (BackendAuthFailed, BackendVersionTooOld):
|
|
342
|
+
raise
|
|
343
|
+
except Exception as exc:
|
|
344
|
+
raise BackendUnreachable(url, str(exc)) from exc
|
|
345
|
+
return extract_vps_ip(data.get("user") or {})
|
|
346
|
+
|
|
316
347
|
async def profile(self) -> UserProfile:
|
|
317
348
|
"""GET {BE}/ml-api/users/profile — the caller's role + department.
|
|
318
349
|
|
|
@@ -1766,7 +1797,7 @@ class BackendClient:
|
|
|
1766
1797
|
"developer-name": "null",
|
|
1767
1798
|
"product": "",
|
|
1768
1799
|
}
|
|
1769
|
-
body: dict[str, str] = {"activity": str(activity)}
|
|
1800
|
+
body: dict[str, str] = {"activity": _fit_activity(str(activity))}
|
|
1770
1801
|
tid = str(task_id or "").strip()
|
|
1771
1802
|
if tid:
|
|
1772
1803
|
body["taskId"] = tid
|
|
@@ -1800,7 +1831,7 @@ class BackendClient:
|
|
|
1800
1831
|
return False
|
|
1801
1832
|
url = f"{ML_GATEWAY_URL.rstrip('/')}/ml-api/agent-activity"
|
|
1802
1833
|
headers = {**self._headers, "accept": "*/*", "developer-name": "null", "product": ""}
|
|
1803
|
-
body: dict[str, str] = {"activity": str(activity), "agentId": aid}
|
|
1834
|
+
body: dict[str, str] = {"activity": _fit_activity(str(activity)), "agentId": aid}
|
|
1804
1835
|
tid = str(task_id or "").strip()
|
|
1805
1836
|
if tid:
|
|
1806
1837
|
body["taskId"] = tid
|
|
@@ -211,10 +211,18 @@ async def report_once(client: "BackendClient | None" = None) -> bool:
|
|
|
211
211
|
"""Collect + PATCH one health snapshot. Returns True on a 2xx. Never raises."""
|
|
212
212
|
payload: dict[str, Any] = {}
|
|
213
213
|
try:
|
|
214
|
-
payload = build_payload()
|
|
215
214
|
if client is None:
|
|
216
215
|
from sdk.client import BackendClient
|
|
217
216
|
client = BackendClient.from_config()
|
|
217
|
+
# Only the assigned VPS may write the VPS row — a laptop running the SDK
|
|
218
|
+
# with the same token used to overwrite it (see sdk.vps_identity).
|
|
219
|
+
from sdk import vps_identity
|
|
220
|
+
verdict = await vps_identity.is_assigned_vps(client)
|
|
221
|
+
if verdict is not True:
|
|
222
|
+
reason = "not_assigned_vps" if verdict is False else "vps_identity_unknown"
|
|
223
|
+
_LAST.update({"at": _now_iso_ms(), "ok": False, "reason": reason, "payload": {}})
|
|
224
|
+
return False
|
|
225
|
+
payload = build_payload()
|
|
218
226
|
ok = await client.report_vps_allocation(payload)
|
|
219
227
|
_LAST.update({
|
|
220
228
|
"at": payload.get("lastPingedAt", ""),
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Is THIS machine the caller's assigned VPS?
|
|
2
|
+
|
|
3
|
+
Health pings (``sdk.vps_health``) and the activity trail (``sdk.activity.shipper``)
|
|
4
|
+
both describe "your VPS", and both are keyed SERVER-side on the ``co_`` token — one
|
|
5
|
+
``contabo_allocation`` row per user. So every machine that ran the SDK with that
|
|
6
|
+
token reported as the VPS: a laptop's CPU, RAM, disk and uptime overwrote the VPS
|
|
7
|
+
row every two hours, and the laptop's events landed in the VPS's audit trail. A
|
|
8
|
+
user with the SDK on a laptop and a VPS had a "VPS" whose boot time was the
|
|
9
|
+
laptop's.
|
|
10
|
+
|
|
11
|
+
The answer comes from comparing the IP the platform has on record for the user's
|
|
12
|
+
VPS (``GET /ml-api/users/profile`` → ``vps.ipAddress``, the field the backend's
|
|
13
|
+
proxy already trusts) with this machine's own addresses. Nothing leaves the
|
|
14
|
+
machine to find those — interface addresses plus the route-to-internet address —
|
|
15
|
+
so the SDK's one-egress-path rule holds.
|
|
16
|
+
|
|
17
|
+
Three answers, and the callers treat them differently:
|
|
18
|
+
``True`` this is the assigned VPS — report.
|
|
19
|
+
``False`` it is not (or the user has no VPS) — do not report.
|
|
20
|
+
``None`` unknown (profile unreachable) — skip this tick, ask again next tick.
|
|
21
|
+
Never reported on a guess, and never cached.
|
|
22
|
+
|
|
23
|
+
``AGENTOS_IS_VPS`` overrides the check: ``1`` for a VPS behind NAT whose public
|
|
24
|
+
address is not on any interface, ``0`` to silence a machine outright.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import ipaddress
|
|
29
|
+
import logging
|
|
30
|
+
import os
|
|
31
|
+
import socket
|
|
32
|
+
import time
|
|
33
|
+
from typing import TYPE_CHECKING, Any
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING: # pragma: no cover — typing only
|
|
36
|
+
from sdk.client import BackendClient
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger("sdk.vps_identity")
|
|
39
|
+
|
|
40
|
+
#: A machine does not change into (or out of) a VPS often; a definite answer is
|
|
41
|
+
#: reused this long so a 5-minute shipper does not fetch the profile every tick.
|
|
42
|
+
CACHE_TTL_S = 6 * 3600.0
|
|
43
|
+
|
|
44
|
+
_TRUTHY = {"1", "true", "yes", "on"}
|
|
45
|
+
_FALSEY = {"0", "false", "no", "off"}
|
|
46
|
+
|
|
47
|
+
# Profile shapes the ML backend has used for the VPS address — the same tolerant
|
|
48
|
+
# set the AI backend reads (devops-bot-ai shared/dependencies._extract_vps_ip).
|
|
49
|
+
_IP_KEYS = ("vpsIp", "vps_ip", "vpsIP", "vpsIpAddress", "vpsAddress", "vpsHost",
|
|
50
|
+
"serverIp", "server_ip", "ipAddress", "ip_address", "ip")
|
|
51
|
+
_NESTED_KEYS = ("vps", "server", "machine", "allocation")
|
|
52
|
+
_NESTED_FIELDS = ("ipAddress", "ip_address", "ip", "host", "address")
|
|
53
|
+
|
|
54
|
+
_cache: dict[str, Any] = {"at": 0.0, "value": None}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _setting(name: str) -> str:
|
|
58
|
+
"""Process env first, then ``~/.agentos/.env`` — same resolution as the panel toggles."""
|
|
59
|
+
val = (os.environ.get(name) or "").strip()
|
|
60
|
+
if val:
|
|
61
|
+
return val
|
|
62
|
+
try:
|
|
63
|
+
from sdk.config import _dotenv_value
|
|
64
|
+
return (_dotenv_value(name) or "").strip()
|
|
65
|
+
except Exception: # noqa: BLE001 — a config read must never break reporting
|
|
66
|
+
return ""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def override() -> bool | None:
|
|
70
|
+
"""``AGENTOS_IS_VPS`` as a bool, or ``None`` when unset/unrecognised."""
|
|
71
|
+
raw = _setting("AGENTOS_IS_VPS").lower()
|
|
72
|
+
if raw in _TRUTHY:
|
|
73
|
+
return True
|
|
74
|
+
if raw in _FALSEY:
|
|
75
|
+
return False
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _as_ip(value: str) -> str:
|
|
80
|
+
"""The bare IP in ``value`` (which may be a URL or ``host:port``), or ``""``."""
|
|
81
|
+
v = value.strip()
|
|
82
|
+
if "://" in v:
|
|
83
|
+
v = v.split("://", 1)[1]
|
|
84
|
+
v = v.split("/", 1)[0]
|
|
85
|
+
if v.startswith("[") and "]" in v: # [ipv6]:port
|
|
86
|
+
v = v[1:v.index("]")]
|
|
87
|
+
elif v.count(":") == 1: # ipv4:port / host:port
|
|
88
|
+
v = v.split(":", 1)[0]
|
|
89
|
+
try:
|
|
90
|
+
return str(ipaddress.ip_address(v))
|
|
91
|
+
except ValueError:
|
|
92
|
+
return ""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def extract_vps_ip(user: dict) -> str:
|
|
96
|
+
"""The assigned VPS IP from a profile ``user`` object; ``""`` when it names none."""
|
|
97
|
+
for k in _IP_KEYS:
|
|
98
|
+
v = user.get(k)
|
|
99
|
+
if isinstance(v, str) and _as_ip(v):
|
|
100
|
+
return _as_ip(v)
|
|
101
|
+
for k in _NESTED_KEYS:
|
|
102
|
+
obj = user.get(k)
|
|
103
|
+
if isinstance(obj, str) and _as_ip(obj):
|
|
104
|
+
return _as_ip(obj)
|
|
105
|
+
if isinstance(obj, dict):
|
|
106
|
+
for f in _NESTED_FIELDS:
|
|
107
|
+
v = obj.get(f)
|
|
108
|
+
if isinstance(v, str) and _as_ip(v):
|
|
109
|
+
return _as_ip(v)
|
|
110
|
+
return ""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def local_addresses() -> set[str]:
|
|
114
|
+
"""Every address this machine holds: interfaces, hostname, route to the internet."""
|
|
115
|
+
found: set[str] = set()
|
|
116
|
+
try:
|
|
117
|
+
import psutil
|
|
118
|
+
for addrs in psutil.net_if_addrs().values():
|
|
119
|
+
for a in addrs:
|
|
120
|
+
if a.family in (socket.AF_INET, socket.AF_INET6):
|
|
121
|
+
ip = _as_ip(a.address.split("%", 1)[0])
|
|
122
|
+
if ip:
|
|
123
|
+
found.add(ip)
|
|
124
|
+
except Exception: # noqa: BLE001 — psutil is an optional extra
|
|
125
|
+
pass
|
|
126
|
+
try:
|
|
127
|
+
for info in socket.getaddrinfo(socket.gethostname(), None):
|
|
128
|
+
ip = _as_ip(str(info[4][0]).split("%", 1)[0])
|
|
129
|
+
if ip:
|
|
130
|
+
found.add(ip)
|
|
131
|
+
except Exception: # noqa: BLE001
|
|
132
|
+
pass
|
|
133
|
+
try:
|
|
134
|
+
from sdk.vps_health import _outbound_ip
|
|
135
|
+
ip = _as_ip(_outbound_ip())
|
|
136
|
+
if ip:
|
|
137
|
+
found.add(ip)
|
|
138
|
+
except Exception: # noqa: BLE001
|
|
139
|
+
pass
|
|
140
|
+
return found
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def is_assigned_vps(client: "BackendClient | None" = None) -> bool | None:
|
|
144
|
+
"""``True`` / ``False`` / ``None`` (unknown) — see the module docstring. Never raises."""
|
|
145
|
+
forced = override()
|
|
146
|
+
if forced is not None:
|
|
147
|
+
return forced
|
|
148
|
+
now = time.monotonic()
|
|
149
|
+
if _cache["value"] is not None and now - _cache["at"] < CACHE_TTL_S:
|
|
150
|
+
return _cache["value"]
|
|
151
|
+
try:
|
|
152
|
+
if client is None:
|
|
153
|
+
from sdk.client import BackendClient
|
|
154
|
+
client = BackendClient.from_config()
|
|
155
|
+
assigned = await client.assigned_vps_ip()
|
|
156
|
+
except Exception as exc: # noqa: BLE001 — offline / auth / not configured
|
|
157
|
+
logger.info("vps_identity.unknown err=%s", exc)
|
|
158
|
+
return None
|
|
159
|
+
if not assigned:
|
|
160
|
+
value, why = False, "no VPS is assigned to this user"
|
|
161
|
+
else:
|
|
162
|
+
value = assigned in local_addresses()
|
|
163
|
+
why = (f"this machine holds {assigned}" if value else
|
|
164
|
+
f"this machine does not hold the assigned VPS address {assigned} — "
|
|
165
|
+
f"set AGENTOS_IS_VPS=1 if it is the VPS behind NAT")
|
|
166
|
+
if value != _cache["value"]:
|
|
167
|
+
logger.info("vps_identity.%s (%s)", "assigned_vps" if value else "not_assigned_vps", why)
|
|
168
|
+
_cache.update(at=now, value=value)
|
|
169
|
+
return value
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def reset_cache() -> None:
|
|
173
|
+
"""Forget the cached answer (tests, and after the user's VPS changes)."""
|
|
174
|
+
_cache.update(at=0.0, value=None)
|
|
@@ -31,8 +31,15 @@ def _isolated(tmp_path, monkeypatch):
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def _queued():
|
|
34
|
+
"""Queued sessions and events in the order the collector RECORDED them.
|
|
35
|
+
|
|
36
|
+
The shipper sends newest-first (live tracking); these tests are about what a
|
|
37
|
+
collector recorded and in which order, so read the rows back by ``seq``.
|
|
38
|
+
"""
|
|
34
39
|
chunk = settings.get_spool().next_chunk()
|
|
35
|
-
|
|
40
|
+
rows = sorted(chunk.rows, key=lambda r: r.seq)
|
|
41
|
+
return ([r.wire for r in rows if r.kind == "session"],
|
|
42
|
+
[r.wire for r in rows if r.kind == "event"])
|
|
36
43
|
|
|
37
44
|
|
|
38
45
|
# ─────────────────────────────── Linux / auditd ───────────────────────────────
|