devops-bot-sdk 1.6.44__tar.gz → 1.6.47__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.44 → devops_bot_sdk-1.6.47}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/collectors/__init__.py +38 -2
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/collectors/linux_auditd.py +11 -1
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/collectors/windows_eventlog.py +28 -1
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/os_setup.py +193 -7
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/service.py +87 -3
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_activity_collectors.py +121 -0
- devops_bot_sdk-1.6.47/tests/test_activity_os_setup.py +283 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_update_timer.py +94 -0
- devops_bot_sdk-1.6.44/tests/test_activity_os_setup.py +0 -151
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/README.md +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/__init__.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/classifier.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/collectors/common.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/collectors/macos_eslogger.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/models.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/recorder.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/redact.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/settings.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/shipper.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/spool.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/browser_profile.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/client.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/config.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/estimator.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/firewall.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/github_fetch.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/journal.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/memory_guard.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/prd_progress.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/providers.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/resume.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/router.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/self_report.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/self_report_local.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/swagger.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/task_types.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/tiers/__init__.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/tiers/mini_agent.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/api.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/swagger.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/url_access.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/write_hook.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_activity_core.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_activity_macos.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_activity_pipeline.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_browser_profile.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_cheap_tier.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_crash_recovery.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_done_transition.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_estimator.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_firewall_open_on_update.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_github_fetch.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_intake_job_outcome.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_journal.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_local_exec_provider.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_mcp_route_contract.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_memory_guard.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_pr_identity.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_prd_artifact.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_project_path_resolution.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_providers.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_resume.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_router.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_run_verdict_is_honest.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_schedule_outputs_and_plans.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_self_report.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_swagger_command.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_tier_flag_parity.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_ui_gate.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_ui_lifespan.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_updater_reports_the_truth.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_url_access.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_vps_health.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_windows_loop_identity.py +0 -0
- {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/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.47
|
|
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.47" # 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
|
|
@@ -14,6 +14,11 @@ The event parser and the processor are pure; only :class:`WindowsEventLogCollect
|
|
|
14
14
|
touches the OS. Processes created by SYSTEM / LOCAL SERVICE / NETWORK SERVICE are
|
|
15
15
|
skipped by default (``ignore_system``): they are thousands of events an hour of
|
|
16
16
|
Windows housekeeping that no human or agent did.
|
|
17
|
+
|
|
18
|
+
The collector's own polling is skipped too: every poll runs ``wevtutil.exe`` from the
|
|
19
|
+
panel process (as the panel's user, not SYSTEM), and each of those is itself a 4688.
|
|
20
|
+
Unfiltered, that is ~8,600 "Administrator ran wevtutil" events a day per VPS, each
|
|
21
|
+
poll recording the previous one.
|
|
17
22
|
"""
|
|
18
23
|
from __future__ import annotations
|
|
19
24
|
|
|
@@ -47,6 +52,9 @@ logger = logging.getLogger("sdk.activity.collectors.windows")
|
|
|
47
52
|
_NS = "{http://schemas.microsoft.com/win/2004/08/events/event}"
|
|
48
53
|
EVENT_IDS = (4688, 4624, 4634, 4647, 4625)
|
|
49
54
|
_MAX_PID_CACHE = 20_000
|
|
55
|
+
#: Executables the collector itself spawns to poll the log.
|
|
56
|
+
_OWN_TOOLS = ("wevtutil.exe",)
|
|
57
|
+
_MAX_OWN_PIDS = 1_000
|
|
50
58
|
|
|
51
59
|
#: Logon types that are a PERSON at a keyboard / remote desktop.
|
|
52
60
|
_INTERACTIVE = {2: SessionKind.CONSOLE, 11: SessionKind.CONSOLE, 10: SessionKind.RDP}
|
|
@@ -123,11 +131,14 @@ class WindowsEventProcessor:
|
|
|
123
131
|
registry: AgentProcessRegistry | None = None,
|
|
124
132
|
book: SessionBook | None = None,
|
|
125
133
|
ignore_system: bool = True,
|
|
134
|
+
self_pid: int | None = None,
|
|
126
135
|
) -> None:
|
|
127
136
|
self._agent_user = agent_user.lower() if agent_user else None
|
|
128
137
|
self._registry = registry or AgentProcessRegistry()
|
|
129
138
|
self._book = book or SessionBook()
|
|
130
139
|
self._ignore_system = ignore_system
|
|
140
|
+
self._self_pid = os.getpid() if self_pid is None else self_pid
|
|
141
|
+
self._own: OrderedDict[int, None] = OrderedDict() # pids of our own polls + children
|
|
131
142
|
self._ppid: OrderedDict[int, int] = OrderedDict()
|
|
132
143
|
self._logons: dict[str, str] = {} # logon id → session key
|
|
133
144
|
self._kinds: dict[str, SessionKind] = {} # logon id → login kind
|
|
@@ -202,6 +213,9 @@ class WindowsEventProcessor:
|
|
|
202
213
|
d = ev.data
|
|
203
214
|
pid, ppid = _hex(d.get("NewProcessId")), _hex(d.get("ProcessId"))
|
|
204
215
|
self._remember_ppid(pid, ppid)
|
|
216
|
+
image = d.get("NewProcessName", "")
|
|
217
|
+
if self._is_own_poll(pid, ppid, image):
|
|
218
|
+
return
|
|
205
219
|
logon_id = d.get("SubjectLogonId", "").lower()
|
|
206
220
|
if self._ignore_system and logon_id in _SYSTEM_LOGON_IDS:
|
|
207
221
|
return
|
|
@@ -232,7 +246,6 @@ class WindowsEventProcessor:
|
|
|
232
246
|
session_key, started=ev.time, verdict=verdict, os_user=user,
|
|
233
247
|
).agent_run_id
|
|
234
248
|
|
|
235
|
-
image = d.get("NewProcessName", "")
|
|
236
249
|
recorder.record_event(ActivityEvent(
|
|
237
250
|
os=Os.WINDOWS, source=Source.WIN_EVENTLOG, category=Category.COMMAND, action="exec",
|
|
238
251
|
occurredAt=ev.time, sessionKey=session_key,
|
|
@@ -243,6 +256,20 @@ class WindowsEventProcessor:
|
|
|
243
256
|
metadata={"parent": d.get("ParentProcessName") or None},
|
|
244
257
|
))
|
|
245
258
|
|
|
259
|
+
def _is_own_poll(self, pid: int, ppid: int, image: str) -> bool:
|
|
260
|
+
"""A ``wevtutil`` this process spawned to poll, or a child of one (its ``conhost``).
|
|
261
|
+
|
|
262
|
+
Matched by parent pid AND image name, so anything else the panel spawns —
|
|
263
|
+
an agent run, ``git``, ``claude`` — is still recorded.
|
|
264
|
+
"""
|
|
265
|
+
name = image.rsplit("\\", 1)[-1].lower()
|
|
266
|
+
if not ((ppid == self._self_pid and name in _OWN_TOOLS) or ppid in self._own):
|
|
267
|
+
return False
|
|
268
|
+
self._own[pid] = None
|
|
269
|
+
while len(self._own) > _MAX_OWN_PIDS:
|
|
270
|
+
self._own.popitem(last=False)
|
|
271
|
+
return True
|
|
272
|
+
|
|
246
273
|
def _remember_ppid(self, pid: int, ppid: int) -> None:
|
|
247
274
|
self._ppid[pid] = ppid
|
|
248
275
|
self._ppid.move_to_end(pid)
|
|
@@ -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,14 @@ 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()
|
|
394
538
|
return False, ("activity audit is not set up — human activity on this VPS is "
|
|
395
|
-
"not being recorded. Run
|
|
539
|
+
f"not being recorded. Run once:\n sudo {_agentos_exe()} "
|
|
540
|
+
"activity-setup\nor by hand:\n" + manual)
|
|
396
541
|
|
|
397
542
|
step = {"linux": _ensure_linux, "windows": _ensure_windows, "darwin": _ensure_macos}
|
|
398
543
|
_, done, err = step[sysname]()
|
|
@@ -415,6 +560,41 @@ def ensure(apply: bool = True) -> tuple[bool, str]:
|
|
|
415
560
|
return False, f"activity audit setup failed: {exc}"
|
|
416
561
|
|
|
417
562
|
|
|
563
|
+
def _sudo_without_password() -> bool:
|
|
564
|
+
"""Can this (unprivileged) process become root with no prompt? Never prompts."""
|
|
565
|
+
if not shutil.which("sudo"):
|
|
566
|
+
return False
|
|
567
|
+
return _run(["sudo", "-n", "true"], timeout=15)[0] == 0
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _ensure_via_sudo() -> tuple[bool, str]:
|
|
571
|
+
"""Run the privileged half as root through ``sudo -n``, as THIS user's ``SUDO_USER``.
|
|
572
|
+
|
|
573
|
+
The root run writes the flag into this user's ``.env`` and grants this user the
|
|
574
|
+
audit log, so the panel (this user) picks both up. Same interpreter, so the same
|
|
575
|
+
installation is what runs — never whatever ``agentos`` is first on root's PATH.
|
|
576
|
+
"""
|
|
577
|
+
before = settings.audit_enabled()
|
|
578
|
+
rc, out = _run(["sudo", "-n", sys.executable, "-m", "sdk.activity.os_setup"], timeout=1800)
|
|
579
|
+
lines = [ln.strip() for ln in out.splitlines() if ln.strip()]
|
|
580
|
+
# cli() prints the outcome first ("activity audit …") and a restart hint after it.
|
|
581
|
+
last = next((ln for ln in lines if ln.startswith("activity audit")), lines[-1] if lines else "")
|
|
582
|
+
if rc != 0:
|
|
583
|
+
logger.warning("activity.os_setup.sudo_failed rc=%s out=%s", rc, out[-300:])
|
|
584
|
+
return False, f"activity audit setup incomplete (via sudo): {last or f'exit {rc}'}"
|
|
585
|
+
changed = settings.audit_enabled() and not before
|
|
586
|
+
return changed, (last if last and "already set up" not in last else
|
|
587
|
+
("activity audit enabled (via sudo)" if changed else ""))
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _agentos_exe() -> str:
|
|
591
|
+
"""Absolute ``agentos`` path, so ``sudo`` finds the venv / ~/.local/bin install."""
|
|
592
|
+
exe = Path(sys.executable).parent / ("agentos.exe" if _system() == "windows" else "agentos")
|
|
593
|
+
if exe.exists():
|
|
594
|
+
return str(exe)
|
|
595
|
+
return shutil.which("agentos") or "agentos"
|
|
596
|
+
|
|
597
|
+
|
|
418
598
|
def cli(argv: list[str]) -> int:
|
|
419
599
|
"""``agentos activity-setup [--check]`` — run (or just report) the OS audit setup."""
|
|
420
600
|
changed, msg = ensure(apply="--check" not in argv)
|
|
@@ -424,5 +604,11 @@ def cli(argv: list[str]) -> int:
|
|
|
424
604
|
print("activity audit: already set up" if settings.audit_enabled()
|
|
425
605
|
else "activity audit: nothing to do on this platform")
|
|
426
606
|
if changed:
|
|
427
|
-
print("restart the panel for it to take effect:
|
|
607
|
+
print("restart the panel for it to take effect: systemctl --user restart agentos-ui "
|
|
608
|
+
"(or sudo systemctl restart agentos-ui for a system service). A running panel "
|
|
609
|
+
"also turns the collector on by itself at its next start.")
|
|
428
610
|
return 1 if "incomplete" in msg or "failed" in msg else 0
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
if __name__ == "__main__": # `sudo -n python -m sdk.activity.os_setup` (see _ensure_via_sudo)
|
|
614
|
+
sys.exit(cli(sys.argv[1:]))
|
|
@@ -9,7 +9,9 @@ Per platform:
|
|
|
9
9
|
``loginctl enable-linger`` so it runs without an active login.
|
|
10
10
|
``Restart=always`` gives crash-recovery; ``enable`` gives boot-start.
|
|
11
11
|
* macOS — a launchd LaunchAgent with ``RunAtLoad`` + ``KeepAlive``.
|
|
12
|
-
* Windows — a Scheduled Task (``schtasks``) that runs the panel at logon
|
|
12
|
+
* Windows — a Scheduled Task (``schtasks``) that runs the panel at logon, plus a
|
|
13
|
+
second, daily task that runs ``agentos-auto update`` (the systemd
|
|
14
|
+
timer's counterpart — without it a Windows box never updated itself).
|
|
13
15
|
|
|
14
16
|
Everything is best-effort and returns ``(ok, message)`` so the caller (the
|
|
15
17
|
``agentos-ui install`` command and the bootstrap scripts) can report cleanly.
|
|
@@ -28,6 +30,7 @@ IS_MAC = sys.platform == "darwin"
|
|
|
28
30
|
|
|
29
31
|
SERVICE_NAME = "agentos-ui" # systemd unit / launchd label base
|
|
30
32
|
WIN_TASK = "AgentOS Control Panel" # Windows scheduled-task name
|
|
33
|
+
WIN_UPDATE_TASK = "AgentOS Self-Update" # Windows counterpart of agentos-update.timer
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
def _exe() -> str:
|
|
@@ -166,7 +169,7 @@ def _auto_exe() -> str:
|
|
|
166
169
|
minimal environment, and picking up a different venv's console script is how
|
|
167
170
|
an updater ends up upgrading the wrong installation.
|
|
168
171
|
"""
|
|
169
|
-
candidate = Path(sys.executable).parent / "agentos-auto"
|
|
172
|
+
candidate = Path(sys.executable).parent / ("agentos-auto.exe" if IS_WINDOWS else "agentos-auto")
|
|
170
173
|
if candidate.exists():
|
|
171
174
|
return str(candidate)
|
|
172
175
|
return shutil.which("agentos-auto") or "agentos-auto"
|
|
@@ -174,6 +177,8 @@ def _auto_exe() -> str:
|
|
|
174
177
|
|
|
175
178
|
def update_timer_installed() -> bool:
|
|
176
179
|
"""Is the daily self-update timer present on this box?"""
|
|
180
|
+
if IS_WINDOWS:
|
|
181
|
+
return _run(["schtasks", "/Query", "/TN", WIN_UPDATE_TASK])[0] == 0
|
|
177
182
|
if not _systemd_available():
|
|
178
183
|
return False
|
|
179
184
|
is_root = hasattr(os, "geteuid") and os.geteuid() == 0
|
|
@@ -185,6 +190,8 @@ def update_timer_installed() -> bool:
|
|
|
185
190
|
def _install_update_timer() -> tuple[bool, str]:
|
|
186
191
|
"""Install (or refresh) the daily self-update timer. Best-effort by design —
|
|
187
192
|
the panel must install even on a box where the timer cannot."""
|
|
193
|
+
if IS_WINDOWS:
|
|
194
|
+
return _install_update_schtask()
|
|
188
195
|
if not _systemd_available():
|
|
189
196
|
return False, "no systemd — skipping the self-update timer"
|
|
190
197
|
home = str(Path.home())
|
|
@@ -378,7 +385,82 @@ def _install_schtask(host: str, port: int) -> tuple[bool, str]:
|
|
|
378
385
|
f"'Run whether user is logged on' + 'Restart on failure' in Task Scheduler.")
|
|
379
386
|
|
|
380
387
|
|
|
388
|
+
def _update_task_xml(exe: str, home: str) -> str:
|
|
389
|
+
"""Task Scheduler XML for the daily self-update — the systemd timer, translated.
|
|
390
|
+
|
|
391
|
+
XML rather than ``schtasks`` flags because the flags cannot express the two
|
|
392
|
+
settings that matter most: ``RandomDelay`` (the fleet must not clone from GitHub
|
|
393
|
+
in the same second) and ``StartWhenAvailable`` (a box that was off at the
|
|
394
|
+
scheduled time updates when it is next up — systemd's ``Persistent=true``).
|
|
395
|
+
|
|
396
|
+
It runs as the installing user with the highest available privileges, same as
|
|
397
|
+
the panel task: ``~/.agentos`` (token, ``.env``) lives in that user's profile, and
|
|
398
|
+
the updater's activity-audit step needs an elevated token to configure auditpol.
|
|
399
|
+
"""
|
|
400
|
+
from xml.sax.saxutils import escape
|
|
401
|
+
|
|
402
|
+
delay_min = max(1, UPDATE_RANDOM_DELAY_S // 60)
|
|
403
|
+
return (
|
|
404
|
+
'<?xml version="1.0" encoding="UTF-16"?>\n'
|
|
405
|
+
'<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">\n'
|
|
406
|
+
" <RegistrationInfo><Description>AgentOS SDK self-update (daily)</Description>"
|
|
407
|
+
"</RegistrationInfo>\n"
|
|
408
|
+
" <Triggers>\n"
|
|
409
|
+
" <CalendarTrigger>\n"
|
|
410
|
+
" <StartBoundary>2026-01-01T03:00:00</StartBoundary>\n"
|
|
411
|
+
f" <RandomDelay>PT{delay_min}M</RandomDelay>\n"
|
|
412
|
+
" <ScheduleByDay><DaysInterval>1</DaysInterval></ScheduleByDay>\n"
|
|
413
|
+
" </CalendarTrigger>\n"
|
|
414
|
+
" </Triggers>\n"
|
|
415
|
+
" <Principals><Principal id=\"Author\">"
|
|
416
|
+
"<LogonType>InteractiveToken</LogonType><RunLevel>HighestAvailable</RunLevel>"
|
|
417
|
+
"</Principal></Principals>\n"
|
|
418
|
+
" <Settings>\n"
|
|
419
|
+
" <StartWhenAvailable>true</StartWhenAvailable>\n"
|
|
420
|
+
" <RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable>\n"
|
|
421
|
+
" <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>\n"
|
|
422
|
+
" <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>\n"
|
|
423
|
+
" <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n"
|
|
424
|
+
" <ExecutionTimeLimit>PT1H</ExecutionTimeLimit>\n"
|
|
425
|
+
" </Settings>\n"
|
|
426
|
+
' <Actions Context="Author"><Exec>'
|
|
427
|
+
f"<Command>{escape(exe)}</Command><Arguments>update</Arguments>"
|
|
428
|
+
f"<WorkingDirectory>{escape(home)}</WorkingDirectory>"
|
|
429
|
+
"</Exec></Actions>\n"
|
|
430
|
+
"</Task>\n"
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _install_update_schtask() -> tuple[bool, str]:
|
|
435
|
+
"""Create (or replace) the daily self-update task. Best-effort, like the timer.
|
|
436
|
+
|
|
437
|
+
A SEPARATE task from the panel's for the same reason the systemd updater is a
|
|
438
|
+
separate unit: the update restarts the panel task, and an updater running inside
|
|
439
|
+
it would be ended part way through its own ``pip install``.
|
|
440
|
+
"""
|
|
441
|
+
import tempfile
|
|
442
|
+
|
|
443
|
+
xml = _update_task_xml(_auto_exe(), str(Path.home()))
|
|
444
|
+
fd, path = tempfile.mkstemp(suffix=".xml")
|
|
445
|
+
try:
|
|
446
|
+
with os.fdopen(fd, "w", encoding="utf-16") as fh: # schtasks /XML wants UTF-16
|
|
447
|
+
fh.write(xml)
|
|
448
|
+
rc, out = _run(["schtasks", "/Create", "/TN", WIN_UPDATE_TASK, "/XML", path, "/F"])
|
|
449
|
+
except OSError as exc:
|
|
450
|
+
return False, f"could not write the self-update task: {exc}"
|
|
451
|
+
finally:
|
|
452
|
+
try:
|
|
453
|
+
os.unlink(path)
|
|
454
|
+
except OSError:
|
|
455
|
+
pass
|
|
456
|
+
if rc != 0:
|
|
457
|
+
return False, f"could not create the self-update task:\n{out}"
|
|
458
|
+
return True, (f"self-update task '{WIN_UPDATE_TASK}' created — "
|
|
459
|
+
f"daily, randomized, catches up after downtime")
|
|
460
|
+
|
|
461
|
+
|
|
381
462
|
def _uninstall_schtask() -> tuple[bool, str]:
|
|
463
|
+
_run(["schtasks", "/Delete", "/TN", WIN_UPDATE_TASK, "/F"])
|
|
382
464
|
_run(["schtasks", "/End", "/TN", WIN_TASK])
|
|
383
465
|
rc, out = _run(["schtasks", "/Delete", "/TN", WIN_TASK, "/F"])
|
|
384
466
|
return (rc == 0), (f"removed scheduled task '{WIN_TASK}'." if rc == 0 else out)
|
|
@@ -388,7 +470,9 @@ def _uninstall_schtask() -> tuple[bool, str]:
|
|
|
388
470
|
|
|
389
471
|
def install(host: str = "0.0.0.0", port: int = 8765) -> tuple[bool, str]:
|
|
390
472
|
if IS_WINDOWS:
|
|
391
|
-
|
|
473
|
+
ok, msg = _install_schtask(host, port)
|
|
474
|
+
timer_ok, timer_msg = _install_update_timer()
|
|
475
|
+
return ok, f"{msg}\n {'' if timer_ok else 'note: '}{timer_msg}"
|
|
392
476
|
if IS_MAC:
|
|
393
477
|
return _install_launchd(host, port)
|
|
394
478
|
if _systemd_available():
|