devops-bot-sdk 1.6.26__tar.gz → 1.6.32__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.26 → devops_bot_sdk-1.6.32}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/devops_bot_sdk.egg-info/SOURCES.txt +7 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/__init__.py +2 -2
- devops_bot_sdk-1.6.32/sdk/browser_profile.py +362 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/cli.py +196 -4
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/client.py +123 -40
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/exceptions.py +17 -0
- devops_bot_sdk-1.6.32/sdk/proof_readers.py +420 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/self_report.py +314 -13
- devops_bot_sdk-1.6.32/sdk/self_report_local.py +533 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/api.py +42 -3
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/static/app.js +11 -3
- devops_bot_sdk-1.6.32/sdk/url_access.py +647 -0
- devops_bot_sdk-1.6.32/tests/test_browser_profile.py +223 -0
- devops_bot_sdk-1.6.32/tests/test_proof_readers.py +263 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_self_report.py +552 -2
- devops_bot_sdk-1.6.32/tests/test_url_access.py +480 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/README.md +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/config.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/static/login.html +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_otp_login.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.26 → devops_bot_sdk-1.6.32}/tests/test_vps_health.py +0 -0
|
@@ -8,6 +8,7 @@ devops_bot_sdk.egg-info/requires.txt
|
|
|
8
8
|
devops_bot_sdk.egg-info/top_level.txt
|
|
9
9
|
sdk/__init__.py
|
|
10
10
|
sdk/agentd.py
|
|
11
|
+
sdk/browser_profile.py
|
|
11
12
|
sdk/cli.py
|
|
12
13
|
sdk/client.py
|
|
13
14
|
sdk/config.py
|
|
@@ -24,16 +25,19 @@ sdk/local_exec.py
|
|
|
24
25
|
sdk/phase_timer.py
|
|
25
26
|
sdk/plan_run.py
|
|
26
27
|
sdk/platform_compat.py
|
|
28
|
+
sdk/proof_readers.py
|
|
27
29
|
sdk/py.typed
|
|
28
30
|
sdk/run_auto.py
|
|
29
31
|
sdk/scheduler.py
|
|
30
32
|
sdk/self_report.py
|
|
33
|
+
sdk/self_report_local.py
|
|
31
34
|
sdk/sse.py
|
|
32
35
|
sdk/task_file.py
|
|
33
36
|
sdk/test.py
|
|
34
37
|
sdk/test_pipeline.py
|
|
35
38
|
sdk/ticket_fields.py
|
|
36
39
|
sdk/updater.py
|
|
40
|
+
sdk/url_access.py
|
|
37
41
|
sdk/vps_health.py
|
|
38
42
|
sdk/agents/__init__.py
|
|
39
43
|
sdk/agents/activity.py
|
|
@@ -117,6 +121,7 @@ tests/test_agent_workspace_settings.py
|
|
|
117
121
|
tests/test_agents_connectors_builder.py
|
|
118
122
|
tests/test_allowed_tools_config.py
|
|
119
123
|
tests/test_auto_agent_dispatch.py
|
|
124
|
+
tests/test_browser_profile.py
|
|
120
125
|
tests/test_client_jira_projects.py
|
|
121
126
|
tests/test_config_token_diagnostics.py
|
|
122
127
|
tests/test_configure_token_owner.py
|
|
@@ -137,6 +142,7 @@ tests/test_otp_login.py
|
|
|
137
142
|
tests/test_phase_timer.py
|
|
138
143
|
tests/test_plan_new_command.py
|
|
139
144
|
tests/test_plan_run_activity.py
|
|
145
|
+
tests/test_proof_readers.py
|
|
140
146
|
tests/test_recon_session_isolation.py
|
|
141
147
|
tests/test_run_auto_agent_only.py
|
|
142
148
|
tests/test_run_outputs.py
|
|
@@ -151,4 +157,5 @@ tests/test_ticket_run_resolve_by_id.py
|
|
|
151
157
|
tests/test_ticket_speed_fixes.py
|
|
152
158
|
tests/test_update_timer.py
|
|
153
159
|
tests/test_updater_extras.py
|
|
160
|
+
tests/test_url_access.py
|
|
154
161
|
tests/test_vps_health.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
|
|
2
2
|
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.32
|
|
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.32" # 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
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""A browser profile the SDK owns, and the operator signs in to once.
|
|
2
|
+
|
|
3
|
+
The generic answer to "read the link I am logged in to" is a browser, and the
|
|
4
|
+
browser has to be one with sessions in it. Neither obvious candidate is:
|
|
5
|
+
|
|
6
|
+
* **Chrome for Testing**, which ``design_verify/bootstrap`` installs for the
|
|
7
|
+
design loop, is a fresh binary with a blank profile. It is signed in to
|
|
8
|
+
nothing, so driving it answers every private link with a login page.
|
|
9
|
+
* **The operator's own Chrome profile** cannot be driven either. Chrome 136 and
|
|
10
|
+
later refuse ``--remote-debugging-port`` while using the default user-data-dir
|
|
11
|
+
— deliberately, because allowing it is how session cookies get stolen — and a
|
|
12
|
+
second Chrome cannot open a profile the running one holds. Copying the profile
|
|
13
|
+
would work and is precisely the cookie-theft shape that decision exists to
|
|
14
|
+
prevent, so it is not done here either.
|
|
15
|
+
|
|
16
|
+
So this module keeps a THIRD profile: ``~/.agentos/browser-profile``, belonging
|
|
17
|
+
to the SDK. ``agentos browser login <url>`` opens a normal, visible window on it;
|
|
18
|
+
the person signs in the way they would anywhere — password, SSO, MFA, whatever
|
|
19
|
+
the site asks — and closes it. The persistent cookies that login writes stay in
|
|
20
|
+
that directory, and every later read runs headless against the same directory and
|
|
21
|
+
is therefore signed in too.
|
|
22
|
+
|
|
23
|
+
Two properties make this the right trade:
|
|
24
|
+
|
|
25
|
+
* **Nothing decrypts anything.** The SDK never reads the person's own cookie
|
|
26
|
+
store. It has its own profile, holding only sessions they deliberately signed
|
|
27
|
+
into for this purpose, in a directory they can delete in one command.
|
|
28
|
+
* **No new dependency and no CDP.** ``chrome --headless=new --dump-dom <url>``
|
|
29
|
+
prints the rendered DOM to stdout. That is the entire mechanism: no WebSocket,
|
|
30
|
+
no Playwright, no driver to keep in step with the browser version.
|
|
31
|
+
|
|
32
|
+
It is genuinely host-agnostic. Nothing here knows what GitHub or Outlook is — a
|
|
33
|
+
site is readable if the person signed in to it, and the list of such sites is
|
|
34
|
+
whatever they chose.
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import asyncio
|
|
39
|
+
import json
|
|
40
|
+
import logging
|
|
41
|
+
import os
|
|
42
|
+
import platform
|
|
43
|
+
import shutil
|
|
44
|
+
import subprocess
|
|
45
|
+
import sys
|
|
46
|
+
from contextlib import suppress
|
|
47
|
+
from dataclasses import dataclass
|
|
48
|
+
from functools import lru_cache
|
|
49
|
+
from datetime import datetime, timezone
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
from urllib.parse import urlparse
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
PROFILE_ENV = "AGENTOS_BROWSER_PROFILE"
|
|
56
|
+
BINARY_ENV = "AGENTOS_BROWSER_BINARY"
|
|
57
|
+
_SANDBOX_ENV = "AGENTOS_BROWSER_NO_SANDBOX"
|
|
58
|
+
|
|
59
|
+
# Chromium family only: ``--dump-dom`` is a Chrome flag and has no Firefox
|
|
60
|
+
# equivalent, and the whole point is one code path rather than a driver per
|
|
61
|
+
# engine. Order is preference — the operator's everyday Chrome first, since a
|
|
62
|
+
# site that works there is the one they will expect to work here.
|
|
63
|
+
_CANDIDATES = (
|
|
64
|
+
"google-chrome", "google-chrome-stable", "chromium", "chromium-browser",
|
|
65
|
+
"microsoft-edge", "microsoft-edge-stable", "brave-browser",
|
|
66
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
67
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# A page is evidence, not an application: give it time to render and settle, then
|
|
71
|
+
# take what is there. `--virtual-time-budget` fast-forwards timers so this costs
|
|
72
|
+
# wall-clock only for the network, not for a site's animations.
|
|
73
|
+
_VIRTUAL_TIME_MS = 12_000
|
|
74
|
+
_FETCH_TIMEOUT_S = 60.0
|
|
75
|
+
_REAP_TIMEOUT_S = 3.0
|
|
76
|
+
_VERSION_TIMEOUT_S = 10.0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def profile_dir() -> Path:
|
|
80
|
+
return Path(os.environ.get(PROFILE_ENV, "") or
|
|
81
|
+
(Path.home() / ".agentos" / "browser-profile")).expanduser()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def sites_file() -> Path:
|
|
85
|
+
"""Hosts the operator signed in to, RECORDED BY US as they do it.
|
|
86
|
+
|
|
87
|
+
Deliberately not derived from the profile's cookie database. `browser status`
|
|
88
|
+
needs to answer "what is this signed in to", and reading cookies to find out
|
|
89
|
+
— even our own — would put cookie-parsing code in the SDK, which is the thing
|
|
90
|
+
this design exists to avoid. A note we wrote ourselves is enough.
|
|
91
|
+
"""
|
|
92
|
+
return profile_dir().parent / "browser-sites.json"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class Fetched:
|
|
97
|
+
"""One headless read. ``html`` empty means it did not happen."""
|
|
98
|
+
html: str = ""
|
|
99
|
+
reason: str = ""
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def ok(self) -> bool:
|
|
103
|
+
return bool(self.html)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def find_browser() -> str:
|
|
107
|
+
"""Path to a Chromium-family browser, or "". Honours ``AGENTOS_BROWSER_BINARY``."""
|
|
108
|
+
override = os.environ.get(BINARY_ENV, "").strip()
|
|
109
|
+
if override:
|
|
110
|
+
return override if (shutil.which(override) or Path(override).exists()) else ""
|
|
111
|
+
for name in _CANDIDATES:
|
|
112
|
+
found = shutil.which(name)
|
|
113
|
+
if found:
|
|
114
|
+
return found
|
|
115
|
+
if name.startswith("/") and Path(name).exists():
|
|
116
|
+
return name
|
|
117
|
+
return ""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def is_available() -> bool:
|
|
121
|
+
return bool(find_browser())
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@lru_cache(maxsize=8)
|
|
125
|
+
def _version_of(binary: str) -> str:
|
|
126
|
+
"""``149.0.7827.200`` from ``chrome --version``, or "".
|
|
127
|
+
|
|
128
|
+
Cached per binary: a browser does not change version inside one process, and
|
|
129
|
+
this is a subprocess on the path of every read. Keyed on the path rather than
|
|
130
|
+
stored in a flag so an override pointing somewhere else is not answered with
|
|
131
|
+
the previous browser's version.
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
out = subprocess.run([binary, "--version"], capture_output=True, text=True,
|
|
135
|
+
timeout=_VERSION_TIMEOUT_S).stdout
|
|
136
|
+
except (OSError, subprocess.SubprocessError):
|
|
137
|
+
return ""
|
|
138
|
+
for word in out.split():
|
|
139
|
+
if word[:1].isdigit() and word.count(".") >= 2:
|
|
140
|
+
return word.strip()
|
|
141
|
+
return ""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _version() -> str:
|
|
145
|
+
binary = find_browser()
|
|
146
|
+
return _version_of(binary) if binary else ""
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def user_agent() -> str:
|
|
150
|
+
"""A non-headless UA for the browser we actually have.
|
|
151
|
+
|
|
152
|
+
New headless still announces ``HeadlessChrome``, and enough sites refuse it
|
|
153
|
+
that a page would come back as a block notice rather than the evidence. Built
|
|
154
|
+
from the installed version rather than hard-coded so it does not quietly go
|
|
155
|
+
stale into exactly the mismatch that gets a request flagged.
|
|
156
|
+
"""
|
|
157
|
+
version = _version() or "140.0.0.0"
|
|
158
|
+
system = sys.platform
|
|
159
|
+
if system == "darwin":
|
|
160
|
+
os_part = "Macintosh; Intel Mac OS X 10_15_7"
|
|
161
|
+
elif system.startswith("win"):
|
|
162
|
+
os_part = "Windows NT 10.0; Win64; x64"
|
|
163
|
+
else:
|
|
164
|
+
os_part = f"X11; Linux {platform.machine() or 'x86_64'}"
|
|
165
|
+
return (f"Mozilla/5.0 ({os_part}) AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
166
|
+
f"Chrome/{version} Safari/537.36")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _base_args() -> list[str]:
|
|
170
|
+
args = [
|
|
171
|
+
f"--user-data-dir={profile_dir()}",
|
|
172
|
+
"--no-first-run",
|
|
173
|
+
"--no-default-browser-check",
|
|
174
|
+
"--disable-background-networking",
|
|
175
|
+
"--disable-sync", # this profile is not anyone's identity
|
|
176
|
+
"--mute-audio",
|
|
177
|
+
]
|
|
178
|
+
if os.environ.get(_SANDBOX_ENV, "").strip() in ("1", "true", "True"):
|
|
179
|
+
# Some containers cannot run the sandbox at all. Off by default: the
|
|
180
|
+
# sandbox is the thing standing between a hostile page and this machine.
|
|
181
|
+
args.append("--no-sandbox")
|
|
182
|
+
return args
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def busy_pid() -> int | None:
|
|
186
|
+
"""The pid holding this profile, or None.
|
|
187
|
+
|
|
188
|
+
Chrome keeps one instance per user-data-dir; a second launch hands its
|
|
189
|
+
arguments to the first and exits, which for a headless read means an empty
|
|
190
|
+
answer with no error. Checking first turns that into a sentence the operator
|
|
191
|
+
can act on. A stale lock from a crash names a pid that is gone, and is
|
|
192
|
+
correctly reported as not busy.
|
|
193
|
+
"""
|
|
194
|
+
lock = profile_dir() / "SingletonLock"
|
|
195
|
+
try:
|
|
196
|
+
target = os.readlink(lock)
|
|
197
|
+
except (OSError, ValueError):
|
|
198
|
+
return None
|
|
199
|
+
# "<hostname>-<pid>"
|
|
200
|
+
_, _, raw = target.rpartition("-")
|
|
201
|
+
try:
|
|
202
|
+
pid = int(raw)
|
|
203
|
+
except ValueError:
|
|
204
|
+
return None
|
|
205
|
+
try:
|
|
206
|
+
os.kill(pid, 0)
|
|
207
|
+
except ProcessLookupError:
|
|
208
|
+
return None
|
|
209
|
+
except PermissionError:
|
|
210
|
+
return pid # alive, owned by someone else
|
|
211
|
+
except OSError:
|
|
212
|
+
return None
|
|
213
|
+
return pid
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
async def fetch_dom(url: str, *, timeout: float = _FETCH_TIMEOUT_S) -> Fetched:
|
|
217
|
+
"""Render ``url`` headless in the signed-in profile and return its DOM.
|
|
218
|
+
|
|
219
|
+
``--dump-dom`` prints the page AFTER scripts have run, which is what makes
|
|
220
|
+
this work on the single-page apps most internal tools are: the HTML a plain
|
|
221
|
+
GET returns is an empty shell, and the content only exists once the app has
|
|
222
|
+
fetched it with the session this profile holds.
|
|
223
|
+
"""
|
|
224
|
+
binary = find_browser()
|
|
225
|
+
if not binary:
|
|
226
|
+
return Fetched(reason=(
|
|
227
|
+
"no Chrome, Chromium, Edge or Brave was found on this machine, so a "
|
|
228
|
+
"link behind a login cannot be opened here. Install one, or set "
|
|
229
|
+
f"{BINARY_ENV} to its path."))
|
|
230
|
+
if not profile_dir().exists():
|
|
231
|
+
return Fetched(reason=(
|
|
232
|
+
"the SDK's browser profile does not exist yet — nothing has been "
|
|
233
|
+
"signed in to. Run `agentos browser login <url>`, sign in, close the "
|
|
234
|
+
"window, and this link becomes readable."))
|
|
235
|
+
pid = busy_pid()
|
|
236
|
+
if pid is not None:
|
|
237
|
+
return Fetched(reason=(
|
|
238
|
+
f"the SDK's browser profile is open in another window (pid {pid}). "
|
|
239
|
+
"Close the window `agentos browser login` opened and try again."))
|
|
240
|
+
|
|
241
|
+
# `user_agent` shells out for the version. Cached, but the first call per
|
|
242
|
+
# binary is a subprocess — off the loop, or one read stalls every other
|
|
243
|
+
# coroutine in the process for as long as the browser takes to answer.
|
|
244
|
+
agent = await asyncio.to_thread(user_agent)
|
|
245
|
+
cmd = [binary, "--headless=new", "--disable-gpu", *_base_args(),
|
|
246
|
+
f"--user-agent={agent}",
|
|
247
|
+
f"--virtual-time-budget={_VIRTUAL_TIME_MS}",
|
|
248
|
+
"--dump-dom", url]
|
|
249
|
+
try:
|
|
250
|
+
proc = await asyncio.create_subprocess_exec(
|
|
251
|
+
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
|
252
|
+
except OSError as exc:
|
|
253
|
+
return Fetched(reason=f"could not start {Path(binary).name} — {exc}")
|
|
254
|
+
try:
|
|
255
|
+
out, err = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
|
256
|
+
except asyncio.TimeoutError:
|
|
257
|
+
proc.kill()
|
|
258
|
+
# Reap it. Without the wait the transport is still open when the loop
|
|
259
|
+
# closes, which surfaces as a "Event loop is closed" warning from
|
|
260
|
+
# somewhere entirely unrelated to the browser.
|
|
261
|
+
with suppress(Exception):
|
|
262
|
+
# BOUNDED. A killed process whose orphaned child still holds the
|
|
263
|
+
# pipes never reports exit, and an unbounded wait here would hang the
|
|
264
|
+
# very call the timeout above exists to bound.
|
|
265
|
+
await asyncio.wait_for(proc.wait(), timeout=_REAP_TIMEOUT_S)
|
|
266
|
+
return Fetched(reason=f"timed out rendering in the browser after {int(timeout)}s.")
|
|
267
|
+
|
|
268
|
+
html = out.decode(errors="replace").strip()
|
|
269
|
+
if not html:
|
|
270
|
+
detail = err.decode(errors="replace").strip().splitlines()
|
|
271
|
+
tail = detail[-1][:200] if detail else f"exit {proc.returncode}"
|
|
272
|
+
return Fetched(reason=f"was opened in the browser but rendered nothing — {tail}")
|
|
273
|
+
return Fetched(html=html)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# ── Signing in ────────────────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
def _load_sites() -> dict:
|
|
279
|
+
try:
|
|
280
|
+
data = json.loads(sites_file().read_text(encoding="utf-8"))
|
|
281
|
+
except Exception: # noqa: BLE001 — absent until the first login
|
|
282
|
+
return {}
|
|
283
|
+
return data if isinstance(data, dict) else {}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def record_login(url: str) -> str:
|
|
287
|
+
"""Note that a login window was opened for this URL's host. Returns the host."""
|
|
288
|
+
host = (urlparse(url).hostname or "").lower()
|
|
289
|
+
if not host:
|
|
290
|
+
return ""
|
|
291
|
+
sites = _load_sites()
|
|
292
|
+
sites[host] = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
293
|
+
try:
|
|
294
|
+
sites_file().parent.mkdir(parents=True, exist_ok=True)
|
|
295
|
+
sites_file().write_text(json.dumps(sites, indent=2, sort_keys=True), encoding="utf-8")
|
|
296
|
+
except OSError as exc:
|
|
297
|
+
logger.warning("browser_profile.sites_write_failed: %s", exc)
|
|
298
|
+
return host
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def logged_in_hosts() -> dict[str, str]:
|
|
302
|
+
"""``{host: when the login window was opened}`` — what we were told, not proof.
|
|
303
|
+
|
|
304
|
+
A session can expire, and a person can open the window and never sign in. So
|
|
305
|
+
this is a record of intent; `agentos report check <url>` is what actually
|
|
306
|
+
answers whether a host is readable right now.
|
|
307
|
+
"""
|
|
308
|
+
return dict(sorted(_load_sites().items()))
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def open_login(url: str) -> tuple[bool, str]:
|
|
312
|
+
"""Open a VISIBLE window on the SDK's profile at ``url``. ``(ok, message)``.
|
|
313
|
+
|
|
314
|
+
Returns as soon as the browser is launched — the point is for a person to
|
|
315
|
+
take it from there, and waiting for them to finish typing a password is not
|
|
316
|
+
this function's business.
|
|
317
|
+
"""
|
|
318
|
+
binary = find_browser()
|
|
319
|
+
if not binary:
|
|
320
|
+
return False, ("no Chrome, Chromium, Edge or Brave found. Install one, or "
|
|
321
|
+
f"set {BINARY_ENV} to its path.")
|
|
322
|
+
pid = busy_pid()
|
|
323
|
+
if pid is not None:
|
|
324
|
+
return False, (f"a window on this profile is already open (pid {pid}). Use it, "
|
|
325
|
+
"or close it and run this again.")
|
|
326
|
+
try:
|
|
327
|
+
profile_dir().mkdir(parents=True, exist_ok=True)
|
|
328
|
+
except OSError as exc:
|
|
329
|
+
return False, f"cannot create {profile_dir()} — {exc}"
|
|
330
|
+
|
|
331
|
+
cmd = [binary, *_base_args(), "--new-window", url]
|
|
332
|
+
try:
|
|
333
|
+
subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
334
|
+
start_new_session=True)
|
|
335
|
+
except OSError as exc:
|
|
336
|
+
return False, f"could not start {Path(binary).name} — {exc}"
|
|
337
|
+
host = record_login(url)
|
|
338
|
+
return True, host
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def forget() -> tuple[bool, str]:
|
|
342
|
+
"""Delete the profile — every session in it. ``(ok, message)``."""
|
|
343
|
+
target = profile_dir()
|
|
344
|
+
if not target.exists():
|
|
345
|
+
return True, f"nothing to remove: {target} does not exist."
|
|
346
|
+
pid = busy_pid()
|
|
347
|
+
if pid is not None:
|
|
348
|
+
return False, (f"a window on this profile is open (pid {pid}). Close it first, "
|
|
349
|
+
"or the deletion leaves a half-written profile behind.")
|
|
350
|
+
try:
|
|
351
|
+
shutil.rmtree(target)
|
|
352
|
+
sites_file().unlink(missing_ok=True)
|
|
353
|
+
except OSError as exc:
|
|
354
|
+
return False, f"could not remove {target} — {exc}"
|
|
355
|
+
return True, f"removed {target} and every session in it."
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
__all__ = [
|
|
359
|
+
"BINARY_ENV", "Fetched", "PROFILE_ENV", "busy_pid", "fetch_dom", "find_browser",
|
|
360
|
+
"forget", "is_available", "logged_in_hosts", "open_login", "profile_dir",
|
|
361
|
+
"record_login", "sites_file", "user_agent",
|
|
362
|
+
]
|