devops-bot-sdk 1.6.46__tar.gz → 1.6.48__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.46 → devops_bot_sdk-1.6.48}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/activity/collectors/__init__.py +38 -2
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/activity/collectors/linux_auditd.py +11 -1
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/activity/os_setup.py +223 -7
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_activity_collectors.py +91 -0
- devops_bot_sdk-1.6.48/tests/test_activity_os_setup.py +313 -0
- devops_bot_sdk-1.6.46/tests/test_activity_os_setup.py +0 -151
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/README.md +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/activity/__init__.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/activity/classifier.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/activity/collectors/common.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/activity/collectors/macos_eslogger.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/activity/collectors/windows_eventlog.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/activity/models.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/activity/recorder.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/activity/redact.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/activity/settings.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/activity/shipper.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/activity/spool.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/browser_profile.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/client.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/config.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/estimator.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/firewall.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/github_fetch.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/journal.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/memory_guard.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/prd_progress.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/providers.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/resume.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/router.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/self_report.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/self_report_local.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/swagger.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/task_types.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/tiers/__init__.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/tiers/mini_agent.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/api.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/ui/swagger.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/url_access.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/sdk/write_hook.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_activity_core.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_activity_macos.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_activity_pipeline.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_browser_profile.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_cheap_tier.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_crash_recovery.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_done_transition.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_estimator.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_firewall_open_on_update.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_github_fetch.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_intake_job_outcome.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_journal.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_local_exec_provider.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_mcp_route_contract.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_memory_guard.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_pr_identity.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_prd_artifact.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_project_path_resolution.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_providers.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_resume.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_router.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_run_verdict_is_honest.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_schedule_outputs_and_plans.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_self_report.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_swagger_command.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_tier_flag_parity.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_ui_gate.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_ui_lifespan.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_updater_reports_the_truth.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_url_access.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_vps_health.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/tests/test_windows_loop_identity.py +0 -0
- {devops_bot_sdk-1.6.46 → devops_bot_sdk-1.6.48}/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.48
|
|
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.48" # 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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""OS-level collectors
|
|
1
|
+
"""OS-level collectors (``AGENTOS_ACTIVITY_AUDIT=1``, switched on by ``os_setup``).
|
|
2
2
|
|
|
3
3
|
Linux auditd sdk.activity.collectors.linux_auditd
|
|
4
4
|
Windows Security log sdk.activity.collectors.windows_eventlog
|
|
@@ -70,9 +70,45 @@ async def run_forever(collector: Any) -> None:
|
|
|
70
70
|
logger.exception("activity collector crashed")
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
async def _setup_then_run() -> None:
|
|
74
|
+
"""Turn the OS audit on (``os_setup.ensure``), then run the collector — no restart.
|
|
75
|
+
|
|
76
|
+
The panel is the one process that is always running, so it is where a box whose
|
|
77
|
+
install/update ran unprivileged gets set up: as root/elevated directly, or through
|
|
78
|
+
passwordless ``sudo`` (Linux). In a thread, because a first run may install auditd.
|
|
79
|
+
"""
|
|
80
|
+
from sdk.activity import os_setup
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
_, msg = await asyncio.to_thread(os_setup.ensure)
|
|
84
|
+
except Exception as exc: # noqa: BLE001 — ensure never raises, but be certain
|
|
85
|
+
logger.warning("activity audit setup crashed: %s", exc)
|
|
86
|
+
return
|
|
87
|
+
if msg:
|
|
88
|
+
logger.warning("%s", msg)
|
|
89
|
+
if not settings.audit_enabled():
|
|
90
|
+
return
|
|
91
|
+
try:
|
|
92
|
+
collector = make_collector()
|
|
93
|
+
except Exception as exc: # noqa: BLE001
|
|
94
|
+
logger.warning("activity collector could not be created: %s", exc)
|
|
95
|
+
return
|
|
96
|
+
if collector is not None:
|
|
97
|
+
logger.info("activity collector starting (%s)", type(collector).__name__)
|
|
98
|
+
await run_forever(collector)
|
|
99
|
+
|
|
100
|
+
|
|
73
101
|
def start() -> asyncio.Task[None] | None:
|
|
74
|
-
if not
|
|
102
|
+
if not settings.enabled():
|
|
75
103
|
return None
|
|
104
|
+
if not settings.audit_enabled():
|
|
105
|
+
from sdk.activity import os_setup
|
|
106
|
+
|
|
107
|
+
# An explicit AGENTOS_ACTIVITY_AUDIT=0 / AGENTOS_NO_AUDIT_SETUP=1 is the
|
|
108
|
+
# operator's decision: respected, nothing touched.
|
|
109
|
+
if os_setup._opted_out():
|
|
110
|
+
return None
|
|
111
|
+
return asyncio.ensure_future(_setup_then_run())
|
|
76
112
|
try:
|
|
77
113
|
collector = make_collector()
|
|
78
114
|
except Exception as exc: # noqa: BLE001
|
|
@@ -159,6 +159,11 @@ class AuditdParser:
|
|
|
159
159
|
inner = re.search(r"msg='([^']*)'", body)
|
|
160
160
|
if inner:
|
|
161
161
|
rec.update({f"m_{k}": v for k, v in _kv(inner.group(1)).items()})
|
|
162
|
+
# acct is quoted when printable and hex-encoded otherwise; as with EXECVE
|
|
163
|
+
# args, only the raw match says which ("dead" is a name AND valid hex).
|
|
164
|
+
for key, raw in _KV.findall(inner.group(1)):
|
|
165
|
+
if key == "acct":
|
|
166
|
+
rec["m_acct"] = _unquote(raw) if raw[:1] in "\"'" else _unhex(raw)
|
|
162
167
|
if rtype == "EXECVE":
|
|
163
168
|
# Quoting is the ONLY thing that tells a literal argument from a hex-encoded
|
|
164
169
|
# one ("beef" is valid hex), so read it from the raw match, not from _kv.
|
|
@@ -217,9 +222,14 @@ class AuditdParser:
|
|
|
217
222
|
terminal = rec.get("m_terminal", "")
|
|
218
223
|
|
|
219
224
|
if not ok:
|
|
225
|
+
# The attempted account name: a failed login has no uid to resolve (it is
|
|
226
|
+
# unset, or an invalid user), so without acct the record says nothing about
|
|
227
|
+
# which names an attacker is trying.
|
|
228
|
+
acct = rec.get("m_acct", "")
|
|
229
|
+
tried = acct if acct and acct not in ("(unknown)", "?") else user
|
|
220
230
|
recorder.record_event(ActivityEvent(
|
|
221
231
|
os=Os.LINUX, source=Source.AUDITD, category=Category.LOGIN, action="login.failed",
|
|
222
|
-
occurredAt=ts, osUser=
|
|
232
|
+
occurredAt=ts, osUser=tried, target=remote, severity=Severity.WARNING,
|
|
223
233
|
actorType=ActorType.UNKNOWN, actorConfidence=0,
|
|
224
234
|
))
|
|
225
235
|
return
|
|
@@ -19,6 +19,17 @@ the same contract:
|
|
|
19
19
|
log anyway.)
|
|
20
20
|
* **never raises** — an update must not fail because of the audit trail.
|
|
21
21
|
|
|
22
|
+
**A panel that runs as an ordinary user** (a user systemd unit, the common case on
|
|
23
|
+
a VPS set up without ``sudo``) was the gap that left most Linux boxes unaudited even
|
|
24
|
+
when that user had root through ``sudo``. Three things now close it:
|
|
25
|
+
|
|
26
|
+
* unprivileged, :func:`ensure` runs its privileged half through ``sudo -n`` when
|
|
27
|
+
sudo needs no password, so nobody has to type a command;
|
|
28
|
+
* under ``sudo`` the flag goes into the INVOKING user's ``~/.agentos/.env``
|
|
29
|
+
(``SUDO_USER``), not ``/root`` — the panel never reads root's home;
|
|
30
|
+
* ``auditd``'s ``log_group`` is set to that user's private group, because
|
|
31
|
+
``audit.log`` is root-only and a collector that cannot read it records nothing.
|
|
32
|
+
|
|
22
33
|
Opt out with ``AGENTOS_NO_AUDIT_SETUP=1``. An operator who set
|
|
23
34
|
``AGENTOS_ACTIVITY_AUDIT=0`` (or ``AGENTOS_ACTIVITY=0``) has decided already; that
|
|
24
35
|
is respected and nothing is touched.
|
|
@@ -35,7 +46,9 @@ import logging
|
|
|
35
46
|
import os
|
|
36
47
|
import platform
|
|
37
48
|
import shutil
|
|
49
|
+
import re
|
|
38
50
|
import subprocess
|
|
51
|
+
import sys
|
|
39
52
|
from pathlib import Path
|
|
40
53
|
|
|
41
54
|
from sdk.activity import settings
|
|
@@ -52,6 +65,8 @@ RULES_TEXT = (
|
|
|
52
65
|
f"-a always,exit -F arch=b64 -S execve -k {RULE_KEY}\n"
|
|
53
66
|
f"-a always,exit -F arch=b32 -S execve -k {RULE_KEY}\n"
|
|
54
67
|
)
|
|
68
|
+
AUDITD_CONF = Path("/etc/audit/auditd.conf")
|
|
69
|
+
AUDIT_LOG = Path("/var/log/audit/audit.log")
|
|
55
70
|
_LINUX_MANUAL = (
|
|
56
71
|
"sudo apt install auditd # or: dnf install audit\n"
|
|
57
72
|
f"sudo tee {RULES_PATH} <<'EOF'\n{RULES_TEXT}EOF\n"
|
|
@@ -112,6 +127,34 @@ def _is_privileged() -> bool:
|
|
|
112
127
|
return hasattr(os, "geteuid") and os.geteuid() == 0
|
|
113
128
|
|
|
114
129
|
|
|
130
|
+
def _panel_user() -> tuple[str, Path, int, int] | None:
|
|
131
|
+
"""``(name, home, uid, gid)`` of the account the panel runs as, if it is NOT root.
|
|
132
|
+
|
|
133
|
+
Under ``sudo`` that is ``SUDO_USER`` — the person who typed the command, whose
|
|
134
|
+
``~/.agentos`` the panel reads. Otherwise it is the current user. ``None`` when the
|
|
135
|
+
panel itself is root, or on a platform without ``pwd``: root needs no grant.
|
|
136
|
+
"""
|
|
137
|
+
try:
|
|
138
|
+
import pwd
|
|
139
|
+
except ImportError: # Windows
|
|
140
|
+
return None
|
|
141
|
+
sudo_user = os.environ.get("SUDO_USER", "").strip()
|
|
142
|
+
try:
|
|
143
|
+
if _is_privileged():
|
|
144
|
+
# Privileged WITHOUT sudo is a root panel (a system unit): nobody to grant,
|
|
145
|
+
# and no other home to write into. Never guess one from the real uid.
|
|
146
|
+
if not sudo_user or sudo_user == "root":
|
|
147
|
+
return None
|
|
148
|
+
pw = pwd.getpwnam(sudo_user)
|
|
149
|
+
else:
|
|
150
|
+
pw = pwd.getpwuid(os.getuid())
|
|
151
|
+
except (KeyError, OSError):
|
|
152
|
+
return None
|
|
153
|
+
if pw.pw_uid == 0:
|
|
154
|
+
return None
|
|
155
|
+
return pw.pw_name, Path(pw.pw_dir), pw.pw_uid, pw.pw_gid
|
|
156
|
+
|
|
157
|
+
|
|
115
158
|
def _run(cmd: list[str], timeout: float = 60.0, env: dict | None = None) -> tuple[int, str]:
|
|
116
159
|
"""``(returncode, output)``; ``(-1, reason)`` when the command could not run."""
|
|
117
160
|
try:
|
|
@@ -123,7 +166,29 @@ def _run(cmd: list[str], timeout: float = 60.0, env: dict | None = None) -> tupl
|
|
|
123
166
|
|
|
124
167
|
|
|
125
168
|
def _env_file() -> Path:
|
|
126
|
-
|
|
169
|
+
"""The ``.env`` the PANEL reads: the sudo-invoking user's, never root's by accident."""
|
|
170
|
+
user = _panel_user() if _is_privileged() else None
|
|
171
|
+
home = user[1] if user else Path.home()
|
|
172
|
+
return home / ".agentos" / ".env"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _flag_value() -> str:
|
|
176
|
+
"""``AGENTOS_ACTIVITY_AUDIT`` as the panel will see it: process env, then its ``.env``.
|
|
177
|
+
|
|
178
|
+
``settings.setting`` reads ``Path.home()``'s ``.env``, which under ``sudo`` is root's
|
|
179
|
+
— not the file the panel reads — so the panel's file is read here directly.
|
|
180
|
+
"""
|
|
181
|
+
val = os.environ.get(AUDIT_FLAG, "").strip()
|
|
182
|
+
if val:
|
|
183
|
+
return val
|
|
184
|
+
try:
|
|
185
|
+
for line in _env_file().read_text(encoding="utf-8").splitlines():
|
|
186
|
+
key, sep, value = line.strip().partition("=")
|
|
187
|
+
if sep and key.strip() == AUDIT_FLAG:
|
|
188
|
+
return value.strip().strip('"').strip("'")
|
|
189
|
+
except OSError:
|
|
190
|
+
pass
|
|
191
|
+
return settings.setting(AUDIT_FLAG)
|
|
127
192
|
|
|
128
193
|
|
|
129
194
|
def _opted_out() -> bool:
|
|
@@ -131,7 +196,7 @@ def _opted_out() -> bool:
|
|
|
131
196
|
return True
|
|
132
197
|
falsey = {"0", "false", "no", "off"}
|
|
133
198
|
return (settings.setting("AGENTOS_ACTIVITY").lower() in falsey
|
|
134
|
-
or
|
|
199
|
+
or _flag_value().lower() in falsey)
|
|
135
200
|
|
|
136
201
|
|
|
137
202
|
def _enable_flag() -> bool:
|
|
@@ -140,14 +205,23 @@ def _enable_flag() -> bool:
|
|
|
140
205
|
Left alone when the flag is set ANYWHERE (process env or .env) — an explicit
|
|
141
206
|
value is the operator's, including an explicit "on".
|
|
142
207
|
"""
|
|
143
|
-
if
|
|
208
|
+
if _flag_value():
|
|
144
209
|
return False
|
|
145
210
|
path = _env_file()
|
|
146
211
|
try:
|
|
212
|
+
created_dir = not path.parent.exists()
|
|
147
213
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
148
214
|
existing = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
149
215
|
sep = "" if (not existing or existing.endswith("\n")) else "\n"
|
|
150
216
|
path.write_text(f"{existing}{sep}{AUDIT_FLAG}=1\n", encoding="utf-8")
|
|
217
|
+
# Written as root into a user's home: hand it back, or the panel (that user)
|
|
218
|
+
# could no longer edit its own .env — and a new ~/.agentos would be root's.
|
|
219
|
+
user = _panel_user() if _is_privileged() else None
|
|
220
|
+
if user:
|
|
221
|
+
_, _, uid, gid = user
|
|
222
|
+
if created_dir:
|
|
223
|
+
os.chown(path.parent, uid, gid)
|
|
224
|
+
os.chown(path, uid, gid)
|
|
151
225
|
return True
|
|
152
226
|
except OSError as exc:
|
|
153
227
|
logger.warning("activity.os_setup.env_write_failed path=%s err=%s", path, exc)
|
|
@@ -215,9 +289,76 @@ def _ensure_linux() -> tuple[bool, list[str], str]:
|
|
|
215
289
|
# Typically a container without audit support in its kernel namespace.
|
|
216
290
|
return bool(done), done, f"audit rules did not load: {out.strip()[-300:]}"
|
|
217
291
|
done.append("loaded audit rules")
|
|
292
|
+
|
|
293
|
+
user = _panel_user()
|
|
294
|
+
if user:
|
|
295
|
+
steps, err = _grant_log_read(user)
|
|
296
|
+
done.extend(steps)
|
|
297
|
+
if err:
|
|
298
|
+
return bool(done), done, err
|
|
218
299
|
return bool(done), done, ""
|
|
219
300
|
|
|
220
301
|
|
|
302
|
+
def _grant_log_read(user: tuple[str, Path, int, int]) -> tuple[list[str], str]:
|
|
303
|
+
"""Let the panel's (non-root) user read ``audit.log``. ``(steps_done, error)``.
|
|
304
|
+
|
|
305
|
+
``log_group`` in ``auditd.conf`` is what survives rotation (auditd applies it to
|
|
306
|
+
every new log file and the directory); the chgrp/chmod makes the CURRENT file
|
|
307
|
+
readable now, without waiting for a rotation. Only a USER-PRIVATE group (named
|
|
308
|
+
like the user) is used: granting a shared group such as ``users`` would hand the
|
|
309
|
+
log of every command on the box to every member of it.
|
|
310
|
+
"""
|
|
311
|
+
import grp
|
|
312
|
+
|
|
313
|
+
name, _, _, gid = user
|
|
314
|
+
try:
|
|
315
|
+
group = grp.getgrgid(gid).gr_name
|
|
316
|
+
except KeyError:
|
|
317
|
+
group = ""
|
|
318
|
+
if group != name:
|
|
319
|
+
return [], (f"{name}'s primary group is {group or gid}, not a private group — "
|
|
320
|
+
f"set log_group in {AUDITD_CONF} to a group only {name} is in")
|
|
321
|
+
done: list[str] = []
|
|
322
|
+
try:
|
|
323
|
+
text = AUDITD_CONF.read_text(encoding="utf-8") if AUDITD_CONF.exists() else ""
|
|
324
|
+
lines = text.splitlines(keepends=True)
|
|
325
|
+
found = changed = False
|
|
326
|
+
for i, line in enumerate(lines):
|
|
327
|
+
if re.match(r"\s*log_group\s*=", line):
|
|
328
|
+
found = True
|
|
329
|
+
if line.split("=", 1)[1].strip() != group:
|
|
330
|
+
lines[i] = f"log_group = {group}\n"
|
|
331
|
+
changed = True
|
|
332
|
+
if not found:
|
|
333
|
+
sep = "" if (not lines or lines[-1].endswith("\n")) else "\n"
|
|
334
|
+
lines.append(f"{sep}log_group = {group}\n")
|
|
335
|
+
changed = True
|
|
336
|
+
if changed:
|
|
337
|
+
AUDITD_CONF.write_text("".join(lines), encoding="utf-8")
|
|
338
|
+
# auditd re-reads its config on SIGHUP; `auditctl --signal` is audit ≥ 3.
|
|
339
|
+
if _run(["auditctl", "--signal", "reload"])[0] != 0:
|
|
340
|
+
_run(["pkill", "-HUP", "-x", "auditd"])
|
|
341
|
+
done.append(f"log_group = {group}")
|
|
342
|
+
for path, bits in ((AUDIT_LOG.parent, 0o050), (AUDIT_LOG, 0o040)):
|
|
343
|
+
if not path.exists():
|
|
344
|
+
continue
|
|
345
|
+
st = path.stat()
|
|
346
|
+
if st.st_gid != gid:
|
|
347
|
+
os.chown(path, -1, gid)
|
|
348
|
+
if st.st_mode & bits != bits:
|
|
349
|
+
os.chmod(path, (st.st_mode & 0o7777) | bits)
|
|
350
|
+
if st.st_gid != gid or st.st_mode & bits != bits:
|
|
351
|
+
if f"{AUDIT_LOG} readable by {name}" not in done:
|
|
352
|
+
done.append(f"{AUDIT_LOG} readable by {name}")
|
|
353
|
+
except OSError as exc:
|
|
354
|
+
return done, f"could not make {AUDIT_LOG} readable by {name}: {exc}"
|
|
355
|
+
return done, ""
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _log_readable() -> bool:
|
|
359
|
+
return os.access(AUDIT_LOG, os.R_OK)
|
|
360
|
+
|
|
361
|
+
|
|
221
362
|
# ── Windows steps ───────────────────────────────────────────────────────────
|
|
222
363
|
|
|
223
364
|
def _ensure_windows() -> tuple[bool, list[str], str]:
|
|
@@ -389,10 +530,17 @@ def ensure(apply: bool = True) -> tuple[bool, str]:
|
|
|
389
530
|
manual = {"linux": _LINUX_MANUAL, "windows": _WIN_MANUAL, "darwin": _MAC_MANUAL}[sysname]
|
|
390
531
|
|
|
391
532
|
if not apply or not _is_privileged():
|
|
392
|
-
|
|
393
|
-
|
|
533
|
+
ready = settings.audit_enabled() and (sysname != "linux" or _log_readable())
|
|
534
|
+
if ready:
|
|
535
|
+
return False, "" # set up already (by hand, or by an earlier run)
|
|
536
|
+
if apply and sysname == "linux" and _sudo_without_password():
|
|
537
|
+
return _ensure_via_sudo()
|
|
538
|
+
import shlex
|
|
539
|
+
|
|
540
|
+
cmd = " ".join(shlex.quote(a) for a in _privileged_argv())
|
|
394
541
|
return False, ("activity audit is not set up — human activity on this VPS is "
|
|
395
|
-
"not being recorded. Run
|
|
542
|
+
f"not being recorded. Run once:\n sudo {cmd}\nor by hand:\n"
|
|
543
|
+
+ manual)
|
|
396
544
|
|
|
397
545
|
step = {"linux": _ensure_linux, "windows": _ensure_windows, "darwin": _ensure_macos}
|
|
398
546
|
_, done, err = step[sysname]()
|
|
@@ -415,6 +563,68 @@ def ensure(apply: bool = True) -> tuple[bool, str]:
|
|
|
415
563
|
return False, f"activity audit setup failed: {exc}"
|
|
416
564
|
|
|
417
565
|
|
|
566
|
+
def _sudo_without_password() -> bool:
|
|
567
|
+
"""Can this (unprivileged) process become root with no prompt? Never prompts."""
|
|
568
|
+
if not shutil.which("sudo"):
|
|
569
|
+
return False
|
|
570
|
+
return _run(["sudo", "-n", "true"], timeout=15)[0] == 0
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _ensure_via_sudo() -> tuple[bool, str]:
|
|
574
|
+
"""Run the privileged half as root through ``sudo -n``, as THIS user's ``SUDO_USER``.
|
|
575
|
+
|
|
576
|
+
The root run writes the flag into this user's ``.env`` and grants this user the
|
|
577
|
+
audit log, so the panel (this user) picks both up. Same interpreter, so the same
|
|
578
|
+
installation is what runs — never whatever ``agentos`` is first on root's PATH.
|
|
579
|
+
"""
|
|
580
|
+
before = settings.audit_enabled()
|
|
581
|
+
rc, out = _run(["sudo", "-n", *_privileged_argv()], timeout=1800)
|
|
582
|
+
lines = [ln.strip() for ln in out.splitlines() if ln.strip()]
|
|
583
|
+
# cli() prints the outcome first ("activity audit …") and a restart hint after it.
|
|
584
|
+
last = next((ln for ln in lines if ln.startswith("activity audit")), lines[-1] if lines else "")
|
|
585
|
+
if rc != 0:
|
|
586
|
+
logger.warning("activity.os_setup.sudo_failed rc=%s out=%s", rc, out[-300:])
|
|
587
|
+
return False, f"activity audit setup incomplete (via sudo): {last or f'exit {rc}'}"
|
|
588
|
+
changed = settings.audit_enabled() and not before
|
|
589
|
+
return changed, (last if last and "already set up" not in last else
|
|
590
|
+
("activity audit enabled (via sudo)" if changed else ""))
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _import_path() -> str:
|
|
594
|
+
"""This process's import path, for the root child.
|
|
595
|
+
|
|
596
|
+
A ``pip install --user`` puts the SDK and its dependencies in the USER's site
|
|
597
|
+
directory (``~/.local/lib/pythonX.Y/site-packages``), which root's interpreter
|
|
598
|
+
never looks in. So ``sudo python3 -m sdk.activity.os_setup`` failed with
|
|
599
|
+
``No module named 'sdk'``, and ``sudo agentos`` failed the same way even with a
|
|
600
|
+
full path. Handing the child exactly the path this process imports from makes it
|
|
601
|
+
import what we import: a --user install, a venv or an editable checkout alike.
|
|
602
|
+
(It runs as root from user-writable directories. That grants nothing, because
|
|
603
|
+
it only happens for a user who already has root through sudo.)
|
|
604
|
+
"""
|
|
605
|
+
# Not the caller's working directory (``-m`` puts it on sys.path): a root child
|
|
606
|
+
# has no business importing whatever happens to sit in the folder someone ran the
|
|
607
|
+
# panel from. Kept only when the SDK itself lives there (an editable checkout).
|
|
608
|
+
sdk_home = str(Path(__file__).resolve().parents[2])
|
|
609
|
+
cwd = os.getcwd()
|
|
610
|
+
seen: list[str] = []
|
|
611
|
+
for p in sys.path:
|
|
612
|
+
if not p or not os.path.isdir(p):
|
|
613
|
+
continue
|
|
614
|
+
p = os.path.abspath(p)
|
|
615
|
+
if p == cwd and p != sdk_home:
|
|
616
|
+
continue
|
|
617
|
+
if p not in seen:
|
|
618
|
+
seen.append(p)
|
|
619
|
+
return os.pathsep.join(seen)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _privileged_argv() -> list[str]:
|
|
623
|
+
"""The command (minus ``sudo``) that runs the privileged half: same interpreter,
|
|
624
|
+
same import path, through ``env`` so ``sudo``'s environment reset cannot drop it."""
|
|
625
|
+
return ["env", f"PYTHONPATH={_import_path()}", sys.executable, "-m", "sdk.activity.os_setup"]
|
|
626
|
+
|
|
627
|
+
|
|
418
628
|
def cli(argv: list[str]) -> int:
|
|
419
629
|
"""``agentos activity-setup [--check]`` — run (or just report) the OS audit setup."""
|
|
420
630
|
changed, msg = ensure(apply="--check" not in argv)
|
|
@@ -424,5 +634,11 @@ def cli(argv: list[str]) -> int:
|
|
|
424
634
|
print("activity audit: already set up" if settings.audit_enabled()
|
|
425
635
|
else "activity audit: nothing to do on this platform")
|
|
426
636
|
if changed:
|
|
427
|
-
print("restart the panel for it to take effect:
|
|
637
|
+
print("restart the panel for it to take effect: systemctl --user restart agentos-ui "
|
|
638
|
+
"(or sudo systemctl restart agentos-ui for a system service). A running panel "
|
|
639
|
+
"also turns the collector on by itself at its next start.")
|
|
428
640
|
return 1 if "incomplete" in msg or "failed" in msg else 0
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
if __name__ == "__main__": # `sudo -n python -m sdk.activity.os_setup` (see _ensure_via_sudo)
|
|
644
|
+
sys.exit(cli(sys.argv[1:]))
|
|
@@ -216,6 +216,36 @@ class TestAuditdParsing:
|
|
|
216
216
|
assert (events[0]["action"], events[0]["severity"]) == ("login.failed", "WARNING")
|
|
217
217
|
assert events[0]["target"] == "198.51.100.9"
|
|
218
218
|
|
|
219
|
+
@pytest.mark.parametrize("acct, expected", [
|
|
220
|
+
('"root"', "root"),
|
|
221
|
+
('"admin"', "admin"),
|
|
222
|
+
('"dead"', "dead"), # quoted: a name, even though it is valid hex
|
|
223
|
+
("6F7261636C65", "oracle"), # unquoted hex: auditd's encoding
|
|
224
|
+
])
|
|
225
|
+
def test_a_failed_login_records_the_account_that_was_tried(self, acct, expected):
|
|
226
|
+
"""Without this every failed SSH login had osUser=None, so a brute force showed
|
|
227
|
+
which IPs attacked but never which accounts they were guessing."""
|
|
228
|
+
p = _parser()
|
|
229
|
+
_feed(p, [
|
|
230
|
+
"type=USER_LOGIN msg=audit(1696852800.000:11): pid=2000 uid=0 auid=4294967295 "
|
|
231
|
+
"ses=4294967295 msg='op=login id=4294967295 acct=" + acct + " exe=\"/usr/sbin/sshd\" "
|
|
232
|
+
"hostname=? addr=198.51.100.9 terminal=ssh res=failed'",
|
|
233
|
+
"type=EOE msg=audit(1696852800.000:11): ",
|
|
234
|
+
])
|
|
235
|
+
_, events = _queued()
|
|
236
|
+
assert events[0]["action"] == "login.failed" and events[0]["osUser"] == expected
|
|
237
|
+
|
|
238
|
+
def test_an_unknown_account_on_a_failed_login_is_not_recorded_as_a_name(self):
|
|
239
|
+
p = _parser(user_name=L._default_user_name) # the real resolver: unset uid → None
|
|
240
|
+
_feed(p, [
|
|
241
|
+
"type=USER_LOGIN msg=audit(1696852800.000:12): pid=2000 uid=0 auid=4294967295 "
|
|
242
|
+
"ses=4294967295 msg='op=login acct=\"(unknown)\" exe=\"/usr/sbin/sshd\" "
|
|
243
|
+
"hostname=? addr=198.51.100.9 terminal=ssh res=failed'",
|
|
244
|
+
"type=EOE msg=audit(1696852800.000:12): ",
|
|
245
|
+
])
|
|
246
|
+
_, events = _queued()
|
|
247
|
+
assert events[0]["action"] == "login.failed" and "osUser" not in events[0]
|
|
248
|
+
|
|
219
249
|
def test_garbage_lines_are_ignored(self):
|
|
220
250
|
p = _parser()
|
|
221
251
|
for junk in ("", "not an audit line", "type=SYSCALL garbage", "type=EOE msg=audit(x:y):"):
|
|
@@ -539,3 +569,64 @@ class TestRunner:
|
|
|
539
569
|
|
|
540
570
|
monkeypatch.setattr(collectors, "make_collector", boom)
|
|
541
571
|
assert collectors.start() is None
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
class TestSetupAtPanelStart:
|
|
575
|
+
"""A box whose install/update ran unprivileged is set up by the always-running panel,
|
|
576
|
+
then the collector starts in the same process — no restart, no command typed."""
|
|
577
|
+
|
|
578
|
+
def test_an_explicit_opt_out_starts_nothing(self, monkeypatch):
|
|
579
|
+
monkeypatch.setenv("AGENTOS_NO_AUDIT_SETUP", "1")
|
|
580
|
+
assert collectors.start() is None
|
|
581
|
+
|
|
582
|
+
def test_an_explicit_off_starts_nothing(self, monkeypatch):
|
|
583
|
+
monkeypatch.delenv("AGENTOS_NO_AUDIT_SETUP", raising=False)
|
|
584
|
+
monkeypatch.setenv("AGENTOS_ACTIVITY_AUDIT", "0")
|
|
585
|
+
assert collectors.start() is None
|
|
586
|
+
|
|
587
|
+
def test_setup_then_the_collector_runs_in_the_same_process(self, monkeypatch):
|
|
588
|
+
import asyncio
|
|
589
|
+
|
|
590
|
+
from sdk.activity import os_setup
|
|
591
|
+
|
|
592
|
+
monkeypatch.delenv("AGENTOS_NO_AUDIT_SETUP", raising=False)
|
|
593
|
+
monkeypatch.delenv("AGENTOS_ACTIVITY_AUDIT", raising=False)
|
|
594
|
+
monkeypatch.setattr(os_setup, "_opted_out", lambda: False)
|
|
595
|
+
ran = []
|
|
596
|
+
|
|
597
|
+
def fake_ensure(apply=True):
|
|
598
|
+
monkeypatch.setenv("AGENTOS_ACTIVITY_AUDIT", "1") # what setup achieves
|
|
599
|
+
return True, "activity audit enabled — installed auditd"
|
|
600
|
+
|
|
601
|
+
class Collector:
|
|
602
|
+
def run(self):
|
|
603
|
+
ran.append("run")
|
|
604
|
+
|
|
605
|
+
def stop(self):
|
|
606
|
+
pass
|
|
607
|
+
|
|
608
|
+
monkeypatch.setattr(os_setup, "ensure", fake_ensure)
|
|
609
|
+
monkeypatch.setattr(collectors, "make_collector", lambda os_name=None: Collector())
|
|
610
|
+
|
|
611
|
+
async def go():
|
|
612
|
+
task = collectors.start()
|
|
613
|
+
assert task is not None
|
|
614
|
+
await task
|
|
615
|
+
asyncio.run(go())
|
|
616
|
+
assert ran == ["run"]
|
|
617
|
+
|
|
618
|
+
def test_no_collector_when_setup_could_not_finish(self, monkeypatch):
|
|
619
|
+
import asyncio
|
|
620
|
+
|
|
621
|
+
from sdk.activity import os_setup
|
|
622
|
+
|
|
623
|
+
monkeypatch.delenv("AGENTOS_NO_AUDIT_SETUP", raising=False)
|
|
624
|
+
monkeypatch.delenv("AGENTOS_ACTIVITY_AUDIT", raising=False)
|
|
625
|
+
monkeypatch.setattr(os_setup, "_opted_out", lambda: False)
|
|
626
|
+
monkeypatch.setattr(os_setup, "ensure", lambda apply=True: (False, "run: sudo agentos activity-setup"))
|
|
627
|
+
monkeypatch.setattr(collectors, "make_collector",
|
|
628
|
+
lambda os_name=None: pytest.fail("collector built without audit"))
|
|
629
|
+
|
|
630
|
+
async def go():
|
|
631
|
+
await collectors.start()
|
|
632
|
+
asyncio.run(go())
|