devops-bot-sdk 1.6.64__tar.gz → 1.6.68__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.64 → devops_bot_sdk-1.6.68}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/models.py +52 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/redact.py +4 -1
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/shipper.py +28 -8
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/spool.py +19 -5
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/service.py +94 -22
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_activity_core.py +1 -1
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_activity_pipeline.py +61 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_update_timer.py +2 -0
- devops_bot_sdk-1.6.68/tests/test_windows_task_paths.py +172 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/README.md +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/__init__.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/classifier.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/collectors/common.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/collectors/linux_auditd.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/collectors/macos_eslogger.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/collectors/windows_eventlog.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/os_setup.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/recorder.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/settings.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/browser_profile.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/client.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/config.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/custom_agents.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/estimator.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/firewall.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/github_fetch.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/journal.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/memory_guard.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/prd_progress.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/providers.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/resume.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/router.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/self_report.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/self_report_local.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/swagger.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/task_types.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/tickets.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/tiers/__init__.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/tiers/mini_agent.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/api.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/swagger.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/url_access.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/vendor/aidraftsman-cli-0.1.0.tgz +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/write_hook.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_activity_collectors.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_activity_macos.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_activity_os_setup.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_crud_cli.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_browser_profile.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_cheap_tier.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_crash_recovery.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_done_transition.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_estimator.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_fallback_cli.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_firewall_open_on_update.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_github_fetch.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_intake_job_outcome.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_journal.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_local_exec_provider.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_mcp_route_contract.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_memory_guard.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_pr_identity.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_prd_artifact.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_project_path_resolution.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_providers.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_resume.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_router.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_run_verdict_is_honest.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_schedule_outputs_and_plans.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_self_report.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_stale_panel_restart.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_swagger_command.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_ticket_cli.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_tier_flag_parity.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_ui_gate.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_ui_lifespan.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_updater_pep668.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_updater_reports_the_truth.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_url_access.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_vps_health.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_windows_loop_identity.py +0 -0
- {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_write_hook.py +0 -0
|
@@ -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.68
|
|
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.68" # 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
|
|
@@ -16,6 +16,7 @@ retried at any hop is stored once.
|
|
|
16
16
|
"""
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
import json
|
|
19
20
|
import os
|
|
20
21
|
import sys
|
|
21
22
|
import time
|
|
@@ -140,6 +141,57 @@ def _wire(obj: Any) -> dict[str, Any]:
|
|
|
140
141
|
return out
|
|
141
142
|
|
|
142
143
|
|
|
144
|
+
#: String caps the relay validates (devops-bot-ai ``modules/vps_activity/schemas.py``).
|
|
145
|
+
#: A value over its cap is a 422, and the shipper drops a row the relay refuses — so
|
|
146
|
+
#: one over-long path or command line used to cost the whole event. Clamp instead.
|
|
147
|
+
WIRE_MAX_CHARS: dict[str, dict[str, int]] = {
|
|
148
|
+
"event": {
|
|
149
|
+
"action": 100,
|
|
150
|
+
"osUser": 255,
|
|
151
|
+
"processName": 255,
|
|
152
|
+
"command": 20_000,
|
|
153
|
+
"target": 4096,
|
|
154
|
+
"agentRunId": 128,
|
|
155
|
+
"sessionKey": 128,
|
|
156
|
+
},
|
|
157
|
+
"session": {
|
|
158
|
+
"osUser": 255,
|
|
159
|
+
"remoteIp": 45,
|
|
160
|
+
"agentRunId": 128,
|
|
161
|
+
"initiatedByKey": 128,
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
_CLAMP_MARK = "…"
|
|
165
|
+
#: Free-form dicts the relay does not cap. Bounded here so that, with the string caps
|
|
166
|
+
#: above, no single row can outgrow a request (2 MB gzipped / 10 MB raw) — a row that
|
|
167
|
+
#: could would be 413'd on its own and dropped.
|
|
168
|
+
WIRE_DICT_FIELDS = ("metadata", "actorReasons")
|
|
169
|
+
MAX_DICT_FIELD_BYTES = 64 * 1024
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def fit_wire_limits(kind: str, wire: dict[str, Any]) -> dict[str, Any]:
|
|
173
|
+
"""``wire`` with every over-long field shortened to fit the relay's limits."""
|
|
174
|
+
out = wire
|
|
175
|
+
|
|
176
|
+
def _set(name: str, value: Any) -> None:
|
|
177
|
+
nonlocal out
|
|
178
|
+
if out is wire:
|
|
179
|
+
out = dict(wire)
|
|
180
|
+
out[name] = value
|
|
181
|
+
|
|
182
|
+
for name, cap in WIRE_MAX_CHARS.get(kind, {}).items():
|
|
183
|
+
value = wire.get(name)
|
|
184
|
+
if isinstance(value, str) and len(value) > cap:
|
|
185
|
+
_set(name, value[: cap - len(_CLAMP_MARK)] + _CLAMP_MARK)
|
|
186
|
+
for name in WIRE_DICT_FIELDS:
|
|
187
|
+
value = wire.get(name)
|
|
188
|
+
if isinstance(value, dict):
|
|
189
|
+
size = len(json.dumps(value, separators=(",", ":"), default=str))
|
|
190
|
+
if size > MAX_DICT_FIELD_BYTES:
|
|
191
|
+
_set(name, {"truncated": True, "originalBytes": size})
|
|
192
|
+
return out
|
|
193
|
+
|
|
194
|
+
|
|
143
195
|
# ── models ───────────────────────────────────────────────────────────────────
|
|
144
196
|
|
|
145
197
|
|
|
@@ -72,6 +72,7 @@ _PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
|
72
72
|
|
|
73
73
|
#: Hard cap so a pathological command line can't bloat the spool.
|
|
74
74
|
MAX_COMMAND_CHARS = 20_000
|
|
75
|
+
TRUNCATED = "…[truncated]"
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
def redact_command(command: str | None) -> str | None:
|
|
@@ -82,5 +83,7 @@ def redact_command(command: str | None) -> str | None:
|
|
|
82
83
|
for pattern, repl in _PATTERNS:
|
|
83
84
|
out = pattern.sub(repl, out)
|
|
84
85
|
if len(out) > MAX_COMMAND_CHARS:
|
|
85
|
-
|
|
86
|
+
# The marker counts toward the cap: the relay 422s anything longer, and the
|
|
87
|
+
# shipper drops a refused event rather than shipping it truncated.
|
|
88
|
+
out = out[: MAX_COMMAND_CHARS - len(TRUNCATED)] + TRUNCATED
|
|
86
89
|
return out
|
|
@@ -7,8 +7,9 @@ Delivery rules — a row leaves the spool ONLY when the relay answered 202 (or i
|
|
|
7
7
|
provably undeliverable):
|
|
8
8
|
|
|
9
9
|
2xx delivered → delete the chunk
|
|
10
|
-
network error, 5xx, 408
|
|
10
|
+
network error, 5xx, 408 retry (short backoff); if still failing, keep
|
|
11
11
|
everything and try again at the next flush
|
|
12
|
+
429 wait out the relay's 60 s window, then retry
|
|
12
13
|
401, 426 token / SDK version needs an operator → keep, stop
|
|
13
14
|
this flush (retrying in a tight loop helps nobody)
|
|
14
15
|
any other 4xx the relay READ the chunk and refused it. Dropping all
|
|
@@ -16,8 +17,9 @@ provably undeliverable):
|
|
|
16
17
|
so the chunk is BISECTED until the offending row is
|
|
17
18
|
isolated; only that single row is dropped.
|
|
18
19
|
|
|
19
|
-
A flush drains the whole spool, chunk by chunk (≤500 events / ≤
|
|
20
|
-
five minutes of full OS auditing is
|
|
20
|
+
A flush drains the whole spool, chunk by chunk (≤500 events / ≤4 MB each), paced under
|
|
21
|
+
the relay's 60-requests-a-minute budget, because five minutes of full OS auditing is
|
|
22
|
+
routinely thousands of events.
|
|
21
23
|
"""
|
|
22
24
|
from __future__ import annotations
|
|
23
25
|
|
|
@@ -36,9 +38,18 @@ logger = logging.getLogger("sdk.activity.shipper")
|
|
|
36
38
|
RETRY_DELAYS_S = (2.0, 8.0)
|
|
37
39
|
#: Safety cap on chunks per flush so one flush can't run away.
|
|
38
40
|
MAX_CHUNKS_PER_FLUSH = 200
|
|
41
|
+
#: The relay allows 60 batches per minute per token. Firing chunks back-to-back blew
|
|
42
|
+
#: through that after ~60 chunks; the 2 s / 8 s retries cannot outlast a 60 s window,
|
|
43
|
+
#: so the flush gave up and a busy host never caught up. Pace below the budget...
|
|
44
|
+
MIN_POST_GAP_S = 1.1
|
|
45
|
+
#: ...and if a 429 still lands (another process on the same token), wait the window
|
|
46
|
+
#: out rather than abandoning the flush.
|
|
47
|
+
RATE_LIMIT_WAIT_S = 61.0
|
|
48
|
+
MAX_RATE_LIMIT_WAITS = 3
|
|
39
49
|
|
|
40
50
|
_NEEDS_OPERATOR = {401, 426}
|
|
41
51
|
_TRANSIENT_4XX = {408, 429}
|
|
52
|
+
_RATE_LIMITED = 429
|
|
42
53
|
|
|
43
54
|
Sleep = Callable[[float], Awaitable[Any]]
|
|
44
55
|
|
|
@@ -73,20 +84,27 @@ def _verdict(status: int | None) -> str:
|
|
|
73
84
|
|
|
74
85
|
async def _attempt(sender: Sender, chunk: Chunk, sleep: Sleep) -> str:
|
|
75
86
|
"""One chunk with short in-flush retries → ``ok`` / ``drop`` / ``stop`` / ``later``."""
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
retry_delays = list(RETRY_DELAYS_S)
|
|
88
|
+
rate_limit_waits = 0
|
|
89
|
+
while True:
|
|
79
90
|
try:
|
|
80
91
|
status: int | None = await sender.post_vps_activity_batch(chunk.to_wire())
|
|
81
92
|
except Exception as exc: # noqa: BLE001 — httpx errors, DNS, TLS, ...
|
|
82
93
|
logger.warning("activity: ship failed (%s); will retry", exc)
|
|
83
94
|
status = None
|
|
95
|
+
if status == _RATE_LIMITED and rate_limit_waits < MAX_RATE_LIMIT_WAITS:
|
|
96
|
+
rate_limit_waits += 1
|
|
97
|
+
logger.info("activity: relay rate-limited; waiting %.0fs", RATE_LIMIT_WAIT_S)
|
|
98
|
+
await sleep(RATE_LIMIT_WAIT_S)
|
|
99
|
+
continue
|
|
84
100
|
verdict = _verdict(status)
|
|
85
101
|
if verdict != "retry":
|
|
86
102
|
if verdict == "stop":
|
|
87
103
|
logger.warning("activity: relay answered %s — needs operator attention", status)
|
|
88
104
|
return verdict
|
|
89
|
-
|
|
105
|
+
if not retry_delays:
|
|
106
|
+
return "later"
|
|
107
|
+
await sleep(retry_delays.pop(0))
|
|
90
108
|
|
|
91
109
|
|
|
92
110
|
async def _deliver(
|
|
@@ -135,10 +153,12 @@ async def flush_once(
|
|
|
135
153
|
from sdk.client import BackendClient
|
|
136
154
|
|
|
137
155
|
sender = BackendClient.from_config()
|
|
138
|
-
for
|
|
156
|
+
for i in range(max_chunks):
|
|
139
157
|
chunk = spool.next_chunk()
|
|
140
158
|
if not chunk:
|
|
141
159
|
break
|
|
160
|
+
if i:
|
|
161
|
+
await sleep(MIN_POST_GAP_S)
|
|
142
162
|
if not await _deliver(spool, sender, chunk, result, sleep):
|
|
143
163
|
break
|
|
144
164
|
else:
|
|
@@ -23,14 +23,21 @@ from dataclasses import dataclass, field
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from typing import Any
|
|
25
25
|
|
|
26
|
+
from sdk.activity.models import fit_wire_limits
|
|
27
|
+
|
|
26
28
|
logger = logging.getLogger("sdk.activity.spool")
|
|
27
29
|
|
|
28
30
|
#: Rows kept at most. ~200k events is days of a busy host; beyond it, shed oldest.
|
|
29
31
|
DEFAULT_MAX_ROWS = 200_000
|
|
30
|
-
#: Wire limits per request — the relay and the gateway enforce the same cap.
|
|
32
|
+
#: Wire limits per request — the relay and the gateway enforce the same row cap.
|
|
31
33
|
MAX_EVENTS_PER_CHUNK = 500
|
|
32
34
|
MAX_SESSIONS_PER_CHUNK = 500
|
|
33
|
-
|
|
35
|
+
#: Uncompressed JSON per request. The relay takes 10 MB decompressed / 2 MB gzipped;
|
|
36
|
+
#: audit JSON gzips well past 2:1, so 4 MB stays under both. A 1 MB cap meant a host
|
|
37
|
+
#: with long command lines sent small chunks, ran into the relay's per-minute request
|
|
38
|
+
#: budget and fell behind until the spool shed its oldest rows. A chunk that still
|
|
39
|
+
#: comes back 413 is bisected by the shipper, so this is a target, not a cliff.
|
|
40
|
+
MAX_CHUNK_BYTES = 4_000_000
|
|
34
41
|
|
|
35
42
|
_SCHEMA = """
|
|
36
43
|
CREATE TABLE IF NOT EXISTS spool (
|
|
@@ -110,7 +117,10 @@ class Spool:
|
|
|
110
117
|
with self._lock:
|
|
111
118
|
self._db.execute(
|
|
112
119
|
"INSERT OR IGNORE INTO spool (kind, key, payload) VALUES ('event', ?, ?)",
|
|
113
|
-
(
|
|
120
|
+
(
|
|
121
|
+
wire["eventId"],
|
|
122
|
+
json.dumps(fit_wire_limits("event", wire), separators=(",", ":")),
|
|
123
|
+
),
|
|
114
124
|
)
|
|
115
125
|
self._trim()
|
|
116
126
|
|
|
@@ -127,7 +137,10 @@ class Spool:
|
|
|
127
137
|
"INSERT INTO spool (kind, key, payload) VALUES ('session', ?, ?) "
|
|
128
138
|
"ON CONFLICT (kind, key) DO UPDATE SET "
|
|
129
139
|
"payload = excluded.payload, rev = rev + 1",
|
|
130
|
-
(
|
|
140
|
+
(
|
|
141
|
+
wire["externalKey"],
|
|
142
|
+
json.dumps(fit_wire_limits("session", wire), separators=(",", ":")),
|
|
143
|
+
),
|
|
131
144
|
)
|
|
132
145
|
self._trim()
|
|
133
146
|
|
|
@@ -176,7 +189,8 @@ class Spool:
|
|
|
176
189
|
break
|
|
177
190
|
if chunk.rows and size + len(payload) > max_bytes:
|
|
178
191
|
break
|
|
179
|
-
|
|
192
|
+
# Re-fit on the way out too: rows queued by an older SDK predate the clamp.
|
|
193
|
+
chunk.rows.append(Row(seq, rev, kind, fit_wire_limits(kind, json.loads(payload))))
|
|
180
194
|
n_events += kind == "event"
|
|
181
195
|
n_sessions += kind == "session"
|
|
182
196
|
size += len(payload)
|
|
@@ -33,25 +33,65 @@ WIN_TASK = "AgentOS Control Panel" # Windows scheduled-task name
|
|
|
33
33
|
WIN_UPDATE_TASK = "AgentOS Self-Update" # Windows counterpart of agentos-update.timer
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def _script_dirs() -> list[Path]:
|
|
37
|
+
"""Where pip puts console scripts for THIS interpreter, most specific first.
|
|
38
|
+
|
|
39
|
+
Next to the interpreter covers a venv (``bin/`` / ``Scripts\\`` holds both
|
|
40
|
+
``python`` and the scripts). It does NOT cover a regular Windows install, where
|
|
41
|
+
``python.exe`` sits in the install root and the scripts in its ``Scripts\\``
|
|
42
|
+
subfolder — ``sysconfig``'s ``scripts`` path is the one that knows. The user
|
|
43
|
+
scheme covers a ``pip install --user``.
|
|
44
|
+
"""
|
|
45
|
+
import sysconfig
|
|
46
|
+
|
|
47
|
+
dirs = [Path(sys.executable).parent]
|
|
48
|
+
for args in (("scripts",), ("scripts", f"{os.name}_user")):
|
|
49
|
+
try:
|
|
50
|
+
p = sysconfig.get_path(*args)
|
|
51
|
+
except KeyError: # scheme not defined on this platform
|
|
52
|
+
continue
|
|
53
|
+
if p and Path(p) not in dirs:
|
|
54
|
+
dirs.append(Path(p))
|
|
55
|
+
return dirs
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _console_script(base: str) -> str | None:
|
|
59
|
+
"""ABSOLUTE path to this SDK's ``base`` console script, or ``None`` if not found."""
|
|
60
|
+
name = f"{base}.exe" if IS_WINDOWS else base
|
|
61
|
+
for d in _script_dirs():
|
|
62
|
+
cand = d / name
|
|
63
|
+
if cand.exists():
|
|
64
|
+
return str(cand)
|
|
65
|
+
found = shutil.which(name)
|
|
66
|
+
return str(Path(found).resolve()) if found else None
|
|
67
|
+
|
|
68
|
+
|
|
36
69
|
def _exe() -> str:
|
|
37
70
|
"""ABSOLUTE path to the ``agentos-ui`` console script.
|
|
38
71
|
|
|
39
|
-
Must be absolute: a service manager (systemd/launchd) does NOT
|
|
40
|
-
user's PATH
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
72
|
+
Must be absolute: a service manager (systemd/launchd/Task Scheduler) does NOT
|
|
73
|
+
search the user's PATH, so a bare ``agentos-ui`` fails with 203/EXEC when the
|
|
74
|
+
script lives somewhere like ``~/.local/bin`` (a ``pip --user`` install) — the
|
|
75
|
+
common case on a VPS — and with 0x80070002 (file not found) on Windows, where
|
|
76
|
+
it lives in ``<python>\\Scripts``. Resolved via :func:`_console_script`; the
|
|
77
|
+
bare name is a last resort kept so a systemd install never hard-fails. The
|
|
78
|
+
Windows installers refuse it (see :func:`_require_absolute`).
|
|
46
79
|
"""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
80
|
+
return _console_script("agentos-ui") or ("agentos-ui.exe" if IS_WINDOWS else "agentos-ui")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _require_absolute(exe: str, task: str) -> str | None:
|
|
84
|
+
"""Refusal message when ``exe`` is not an absolute path, else ``None``.
|
|
85
|
+
|
|
86
|
+
A scheduled task with a bare program name is created without complaint and then
|
|
87
|
+
never starts — Last Result 0x80070002, no panel, no activity trail, nothing on
|
|
88
|
+
screen. Refusing here turns that silence into a message the updater prints.
|
|
89
|
+
"""
|
|
90
|
+
if Path(exe).is_absolute():
|
|
91
|
+
return None
|
|
92
|
+
return (f"not creating task '{task}': could not find {exe} for this Python "
|
|
93
|
+
f"({sys.executable}). Reinstall the SDK with that interpreter and re-run "
|
|
94
|
+
f"`agentos-auto update`.")
|
|
55
95
|
|
|
56
96
|
|
|
57
97
|
def _run(argv: list[str]) -> tuple[int, str]:
|
|
@@ -169,10 +209,7 @@ def _auto_exe() -> str:
|
|
|
169
209
|
minimal environment, and picking up a different venv's console script is how
|
|
170
210
|
an updater ends up upgrading the wrong installation.
|
|
171
211
|
"""
|
|
172
|
-
|
|
173
|
-
if candidate.exists():
|
|
174
|
-
return str(candidate)
|
|
175
|
-
return shutil.which("agentos-auto") or "agentos-auto"
|
|
212
|
+
return _console_script("agentos-auto") or ("agentos-auto.exe" if IS_WINDOWS else "agentos-auto")
|
|
176
213
|
|
|
177
214
|
|
|
178
215
|
def update_timer_installed() -> bool:
|
|
@@ -374,7 +411,11 @@ def _uninstall_launchd() -> tuple[bool, str]:
|
|
|
374
411
|
# ── Windows (Scheduled Task) ────────────────────────────────────────────────
|
|
375
412
|
|
|
376
413
|
def _install_schtask(host: str, port: int) -> tuple[bool, str]:
|
|
377
|
-
|
|
414
|
+
exe = _exe()
|
|
415
|
+
refusal = _require_absolute(exe, WIN_TASK)
|
|
416
|
+
if refusal:
|
|
417
|
+
return False, refusal
|
|
418
|
+
tr = f'"{exe}" --host {host} --port {port} --no-browser'
|
|
378
419
|
rc, out = _run(["schtasks", "/Create", "/TN", WIN_TASK, "/TR", tr,
|
|
379
420
|
"/SC", "ONLOGON", "/RL", "HIGHEST", "/F"])
|
|
380
421
|
if rc != 0:
|
|
@@ -440,7 +481,11 @@ def _install_update_schtask() -> tuple[bool, str]:
|
|
|
440
481
|
"""
|
|
441
482
|
import tempfile
|
|
442
483
|
|
|
443
|
-
|
|
484
|
+
exe = _auto_exe()
|
|
485
|
+
refusal = _require_absolute(exe, WIN_UPDATE_TASK)
|
|
486
|
+
if refusal:
|
|
487
|
+
return False, refusal
|
|
488
|
+
xml = _update_task_xml(exe, str(Path.home()))
|
|
444
489
|
fd, path = tempfile.mkstemp(suffix=".xml")
|
|
445
490
|
try:
|
|
446
491
|
with os.fdopen(fd, "w", encoding="utf-16") as fh: # schtasks /XML wants UTF-16
|
|
@@ -459,6 +504,29 @@ def _install_update_schtask() -> tuple[bool, str]:
|
|
|
459
504
|
f"daily, randomized, catches up after downtime")
|
|
460
505
|
|
|
461
506
|
|
|
507
|
+
def _schtask_command(task: str) -> str | None:
|
|
508
|
+
"""The program ``task`` runs, from its XML definition; ``None`` if unreadable."""
|
|
509
|
+
import html
|
|
510
|
+
|
|
511
|
+
rc, out = _run(["schtasks", "/Query", "/TN", task, "/XML"])
|
|
512
|
+
if rc != 0:
|
|
513
|
+
return None
|
|
514
|
+
m = re.search(r"<Command>(.*?)</Command>", out, re.S)
|
|
515
|
+
return html.unescape(m.group(1)).strip().strip('"') if m else None
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _schtask_points_nowhere(task: str) -> bool:
|
|
519
|
+
"""True when ``task`` exists but runs a program that is not an existing file.
|
|
520
|
+
|
|
521
|
+
Boxes installed before :func:`_script_dirs` existed have a panel task whose
|
|
522
|
+
program is the bare ``agentos-ui.exe``. It shows "Ready", never runs, and the
|
|
523
|
+
ordinary update path ("installed + unchanged") only ever re-runs it — so without
|
|
524
|
+
this check those boxes would stay broken through every update.
|
|
525
|
+
"""
|
|
526
|
+
cmd = _schtask_command(task)
|
|
527
|
+
return cmd is not None and not (Path(cmd).is_absolute() and Path(cmd).is_file())
|
|
528
|
+
|
|
529
|
+
|
|
462
530
|
def _uninstall_schtask() -> tuple[bool, str]:
|
|
463
531
|
_run(["schtasks", "/Delete", "/TN", WIN_UPDATE_TASK, "/F"])
|
|
464
532
|
_run(["schtasks", "/End", "/TN", WIN_TASK])
|
|
@@ -771,7 +839,7 @@ def ensure(restart_running: bool = False) -> tuple[bool, str]:
|
|
|
771
839
|
if not is_installed():
|
|
772
840
|
ok, msg = install() # create at the current privilege
|
|
773
841
|
return ok, (f"service created — {msg}" if ok else f"could not create service: {msg}")
|
|
774
|
-
if not update_timer_installed():
|
|
842
|
+
if not update_timer_installed() or (IS_WINDOWS and _schtask_points_nowhere(WIN_UPDATE_TASK)):
|
|
775
843
|
_install_update_timer()
|
|
776
844
|
if _needs_root_for_existing():
|
|
777
845
|
return False, ("a system service is installed — run `sudo agentos-ui install` "
|
|
@@ -783,6 +851,10 @@ def ensure(restart_running: bool = False) -> tuple[bool, str]:
|
|
|
783
851
|
if restart_running:
|
|
784
852
|
rok, rmsg = refresh()
|
|
785
853
|
return rok, (_with_token_warning(rmsg) if rok else rmsg)
|
|
854
|
+
if IS_WINDOWS and _schtask_points_nowhere(WIN_TASK):
|
|
855
|
+
rok, rmsg = refresh() # rebuilds the task with an absolute path
|
|
856
|
+
return rok, (_with_token_warning(f"panel task pointed at a missing program — "
|
|
857
|
+
f"rebuilt; {rmsg}") if rok else rmsg)
|
|
786
858
|
if _is_active():
|
|
787
859
|
stale = _restart_if_stale()
|
|
788
860
|
if stale is not None:
|
|
@@ -176,7 +176,7 @@ class TestRedaction:
|
|
|
176
176
|
def test_is_length_capped(self):
|
|
177
177
|
out = redact_command("a" * (MAX_COMMAND_CHARS + 500))
|
|
178
178
|
assert out.endswith("…[truncated]")
|
|
179
|
-
assert len(out)
|
|
179
|
+
assert len(out) == MAX_COMMAND_CHARS # the relay 422s anything longer
|
|
180
180
|
|
|
181
181
|
|
|
182
182
|
class TestClassifier:
|
|
@@ -66,6 +66,45 @@ class TestSpool:
|
|
|
66
66
|
_sess("S1", actorType="HUMAN", endedAt="2026-10-09T12:00:00.000Z")
|
|
67
67
|
]
|
|
68
68
|
|
|
69
|
+
def test_over_long_fields_are_clamped_to_the_relay_caps_not_left_to_be_refused(self, spool):
|
|
70
|
+
from sdk.activity.models import WIRE_MAX_CHARS
|
|
71
|
+
|
|
72
|
+
spool.add_event(_ev("E1", command="c" * 30_000, target="/t" * 5000, processName="p" * 999))
|
|
73
|
+
spool.add_session(_sess("S1", osUser="u" * 999))
|
|
74
|
+
chunk = spool.next_chunk()
|
|
75
|
+
ev, sess = chunk.events[0], chunk.sessions[0]
|
|
76
|
+
for name in ("command", "target", "processName"):
|
|
77
|
+
assert len(ev[name]) == WIRE_MAX_CHARS["event"][name]
|
|
78
|
+
assert len(sess["osUser"]) == WIRE_MAX_CHARS["session"]["osUser"]
|
|
79
|
+
assert ev["action"] == "exec" # short fields untouched
|
|
80
|
+
|
|
81
|
+
def test_an_oversized_metadata_dict_is_replaced_so_the_row_still_ships(self, spool):
|
|
82
|
+
from sdk.activity.models import MAX_DICT_FIELD_BYTES
|
|
83
|
+
|
|
84
|
+
spool.add_event(_ev("E1", metadata={"blob": "x" * (MAX_DICT_FIELD_BYTES * 2)}))
|
|
85
|
+
spool.add_event(_ev("E2", metadata={"small": 1}))
|
|
86
|
+
events = spool.next_chunk().events
|
|
87
|
+
assert events[0]["metadata"]["truncated"] is True
|
|
88
|
+
assert events[0]["metadata"]["originalBytes"] > MAX_DICT_FIELD_BYTES
|
|
89
|
+
assert events[1]["metadata"] == {"small": 1}
|
|
90
|
+
|
|
91
|
+
def test_a_single_worst_case_row_fits_comfortably_in_one_request(self, spool):
|
|
92
|
+
from sdk.activity.models import MAX_DICT_FIELD_BYTES
|
|
93
|
+
|
|
94
|
+
spool.add_event(_ev(
|
|
95
|
+
"E1", command="c" * 99_999, target="t" * 99_999, processName="p" * 9_999,
|
|
96
|
+
metadata={"blob": "x" * (MAX_DICT_FIELD_BYTES * 10)},
|
|
97
|
+
))
|
|
98
|
+
(row,) = spool.next_chunk().rows
|
|
99
|
+
import json as _json
|
|
100
|
+
|
|
101
|
+
assert len(_json.dumps(row.wire)) < 200_000 # vs 2 MB gzipped / 10 MB raw at the relay
|
|
102
|
+
|
|
103
|
+
def test_default_chunk_byte_cap_stays_under_the_relay_body_limit(self):
|
|
104
|
+
from sdk.activity import spool as spool_mod
|
|
105
|
+
|
|
106
|
+
assert 1_000_000 < spool_mod.MAX_CHUNK_BYTES < 10 * 1024 * 1024
|
|
107
|
+
|
|
69
108
|
def test_rows_come_back_in_arrival_order_session_first(self, spool):
|
|
70
109
|
spool.add_session(_sess("S1"))
|
|
71
110
|
spool.add_event(_ev("E1"))
|
|
@@ -231,6 +270,28 @@ class TestShipper:
|
|
|
231
270
|
result = _run(shipper.flush_once(spool, sender, sleep=Sleeps()))
|
|
232
271
|
assert result.shipped_rows == 1
|
|
233
272
|
|
|
273
|
+
def test_a_429_waits_out_the_relay_window_instead_of_giving_up(self, spool):
|
|
274
|
+
spool.add_event(_ev("E1"))
|
|
275
|
+
sender = FakeSender(script=[429, 429, 429, 202])
|
|
276
|
+
sleeps = Sleeps()
|
|
277
|
+
result = _run(shipper.flush_once(spool, sender, sleep=sleeps))
|
|
278
|
+
assert result.shipped_rows == 1 and result.stopped is None
|
|
279
|
+
assert sleeps.delays == [shipper.RATE_LIMIT_WAIT_S] * 3
|
|
280
|
+
|
|
281
|
+
def test_endless_429s_keep_the_data_for_the_next_flush(self, spool):
|
|
282
|
+
spool.add_event(_ev("E1"))
|
|
283
|
+
sender = FakeSender(default=429)
|
|
284
|
+
result = _run(shipper.flush_once(spool, sender, sleep=Sleeps()))
|
|
285
|
+
assert result.stopped == "later" and spool.count() == 1
|
|
286
|
+
|
|
287
|
+
def test_chunks_are_paced_under_the_relay_request_budget(self, spool):
|
|
288
|
+
for i in range(1200):
|
|
289
|
+
spool.add_event(_ev(f"E{i:05d}"))
|
|
290
|
+
sleeps = Sleeps()
|
|
291
|
+
_run(shipper.flush_once(spool, FakeSender(), sleep=sleeps))
|
|
292
|
+
assert sleeps.delays == [shipper.MIN_POST_GAP_S] * 2 # 3 chunks, 2 gaps
|
|
293
|
+
assert shipper.MIN_POST_GAP_S * 60 > 60 # stays under 60 requests a minute
|
|
294
|
+
|
|
234
295
|
@pytest.mark.parametrize("status", [401, 426])
|
|
235
296
|
def test_token_or_version_problems_stop_without_retrying_or_dropping(self, spool, status):
|
|
236
297
|
spool.add_event(_ev("E1"))
|
|
@@ -177,6 +177,7 @@ class TestWindowsUpdateTask:
|
|
|
177
177
|
|
|
178
178
|
def test_creates_the_task_from_a_utf16_xml_file(self, monkeypatch):
|
|
179
179
|
monkeypatch.setattr(service, "IS_WINDOWS", True)
|
|
180
|
+
monkeypatch.setattr(service, "_auto_exe", lambda: "/py/Scripts/agentos-auto.exe")
|
|
180
181
|
seen = {}
|
|
181
182
|
|
|
182
183
|
def fake_run(argv):
|
|
@@ -194,6 +195,7 @@ class TestWindowsUpdateTask:
|
|
|
194
195
|
|
|
195
196
|
def test_a_failed_create_is_reported_not_raised(self, monkeypatch):
|
|
196
197
|
monkeypatch.setattr(service, "IS_WINDOWS", True)
|
|
198
|
+
monkeypatch.setattr(service, "_auto_exe", lambda: "/py/Scripts/agentos-auto.exe")
|
|
197
199
|
monkeypatch.setattr(service, "_run", lambda argv: (1, "Access is denied."))
|
|
198
200
|
ok, msg = service._install_update_timer()
|
|
199
201
|
assert ok is False and "Access is denied." in msg
|