devops-bot-sdk 1.6.28__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.28 → devops_bot_sdk-1.6.32}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/devops_bot_sdk.egg-info/SOURCES.txt +4 -0
- {devops_bot_sdk-1.6.28 → 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.28 → devops_bot_sdk-1.6.32}/sdk/cli.py +145 -3
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/self_report.py +130 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/self_report_local.py +21 -119
- 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.28 → devops_bot_sdk-1.6.32}/tests/test_self_report.py +136 -2
- devops_bot_sdk-1.6.32/tests/test_url_access.py +480 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/README.md +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/client.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/config.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/api.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/static/login.html +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_otp_login.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.6.28 → devops_bot_sdk-1.6.32}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.28 → 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
|
|
@@ -36,6 +37,7 @@ sdk/test.py
|
|
|
36
37
|
sdk/test_pipeline.py
|
|
37
38
|
sdk/ticket_fields.py
|
|
38
39
|
sdk/updater.py
|
|
40
|
+
sdk/url_access.py
|
|
39
41
|
sdk/vps_health.py
|
|
40
42
|
sdk/agents/__init__.py
|
|
41
43
|
sdk/agents/activity.py
|
|
@@ -119,6 +121,7 @@ tests/test_agent_workspace_settings.py
|
|
|
119
121
|
tests/test_agents_connectors_builder.py
|
|
120
122
|
tests/test_allowed_tools_config.py
|
|
121
123
|
tests/test_auto_agent_dispatch.py
|
|
124
|
+
tests/test_browser_profile.py
|
|
122
125
|
tests/test_client_jira_projects.py
|
|
123
126
|
tests/test_config_token_diagnostics.py
|
|
124
127
|
tests/test_configure_token_owner.py
|
|
@@ -154,4 +157,5 @@ tests/test_ticket_run_resolve_by_id.py
|
|
|
154
157
|
tests/test_ticket_speed_fixes.py
|
|
155
158
|
tests/test_update_timer.py
|
|
156
159
|
tests/test_updater_extras.py
|
|
160
|
+
tests/test_url_access.py
|
|
157
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
|
+
]
|
|
@@ -143,12 +143,16 @@ def main() -> None:
|
|
|
143
143
|
_report_command(rest)
|
|
144
144
|
return
|
|
145
145
|
|
|
146
|
+
if noun == "browser":
|
|
147
|
+
_browser_command(rest)
|
|
148
|
+
return
|
|
149
|
+
|
|
146
150
|
_print_usage()
|
|
147
151
|
sys.exit(1)
|
|
148
152
|
|
|
149
153
|
|
|
150
154
|
def _print_usage() -> None:
|
|
151
|
-
print("usage: agentos <configure|chat|agent|integration|default-agents|agentd|github|report> ...")
|
|
155
|
+
print("usage: agentos <configure|chat|agent|integration|default-agents|agentd|github|report|browser> ...")
|
|
152
156
|
print()
|
|
153
157
|
print(" agentos configure [--rotate]")
|
|
154
158
|
print(" agentos chat [--product-id ID] [--pv-id ID] [--no-autorun] # requirements → ticket")
|
|
@@ -159,6 +163,7 @@ def _print_usage() -> None:
|
|
|
159
163
|
print(" agentos agentd <start [--foreground]|status>")
|
|
160
164
|
print(" agentos github <ingest [--days N]|status> # PR ingestion; --days N backfills N days")
|
|
161
165
|
print(" agentos report <submit|list|show ID> # report work Jira/GitHub never saw")
|
|
166
|
+
print(" agentos browser <login URL|status|logout> # sign in once; private links become readable")
|
|
162
167
|
|
|
163
168
|
|
|
164
169
|
# ── agent ────────────────────────────────────────────────────────────────
|
|
@@ -732,6 +737,7 @@ def _report_usage() -> None:
|
|
|
732
737
|
print(" agentos report list [--limit N]")
|
|
733
738
|
print(" agentos report show <ID>")
|
|
734
739
|
print(" agentos report formats # what a proof can be")
|
|
740
|
+
print(" agentos report check <URL> # can this box open it?")
|
|
735
741
|
print()
|
|
736
742
|
print(" Report work that never reached Jira or GitHub, attach proofs, and have the")
|
|
737
743
|
print(" submission scored against them. It then appears in your daily and overall")
|
|
@@ -741,6 +747,11 @@ def _report_usage() -> None:
|
|
|
741
747
|
print(" --text the report body; --file reads it from a file instead")
|
|
742
748
|
print(" --proof repeatable — a local file path OR an http(s) URL")
|
|
743
749
|
print(" --no-wait submit and exit without waiting for the verdict")
|
|
750
|
+
print()
|
|
751
|
+
print(" A proof URL is read HERE, with your own credentials, before it is submitted —")
|
|
752
|
+
print(" a private PR or an internal page the backend could only 404 on. `check` shows")
|
|
753
|
+
print(" which route opens a link (gh/az token, a host token, or your signed-in")
|
|
754
|
+
print(" browser) without submitting anything.")
|
|
744
755
|
|
|
745
756
|
|
|
746
757
|
def _report_flag(args: list[str], name: str) -> list[str]:
|
|
@@ -758,6 +769,122 @@ def _report_flag(args: list[str], name: str) -> list[str]:
|
|
|
758
769
|
return out
|
|
759
770
|
|
|
760
771
|
|
|
772
|
+
def _browser_usage() -> None:
|
|
773
|
+
print("usage: agentos browser login <URL> # sign in once; the session is kept")
|
|
774
|
+
print(" agentos browser status # what this profile is signed in to")
|
|
775
|
+
print(" agentos browser logout [--yes] # delete the profile and every session")
|
|
776
|
+
print()
|
|
777
|
+
print(" A private link is only evidence if something here can open it. Tokens cover")
|
|
778
|
+
print(" GitHub and Microsoft; everything else — Jira, Confluence, Notion, an internal")
|
|
779
|
+
print(" dashboard — is covered by a browser that is signed in.")
|
|
780
|
+
print()
|
|
781
|
+
print(" That browser is NOT your everyday Chrome profile: Chrome refuses to be driven")
|
|
782
|
+
print(" while using it, and copying it would be cookie theft by another name. The SDK")
|
|
783
|
+
print(" keeps its own profile instead, at ~/.agentos/browser-profile, holding only the")
|
|
784
|
+
print(" sessions you deliberately sign in to here. `logout` deletes all of it.")
|
|
785
|
+
print()
|
|
786
|
+
print(" login opens a normal window. Sign in the way you normally would — password,")
|
|
787
|
+
print(" SSO, MFA, all of it — then close the window. Later reads run headless against")
|
|
788
|
+
print(" the same profile, so they are signed in too.")
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def _browser_command(args: list[str]) -> None:
|
|
792
|
+
"""``agentos browser login|status|logout`` — the signed-in half of link access."""
|
|
793
|
+
from sdk import browser_profile as bp
|
|
794
|
+
|
|
795
|
+
verb = args[0] if args else None
|
|
796
|
+
rest = args[1:]
|
|
797
|
+
|
|
798
|
+
if verb == "login" and rest and not rest[0].startswith("-"):
|
|
799
|
+
url = rest[0]
|
|
800
|
+
if not (url.startswith("http://") or url.startswith("https://")):
|
|
801
|
+
print(f"error: not an http(s) URL: {url}")
|
|
802
|
+
sys.exit(1)
|
|
803
|
+
ok, message = bp.open_login(url)
|
|
804
|
+
if not ok:
|
|
805
|
+
print(f"error: {message}")
|
|
806
|
+
sys.exit(1)
|
|
807
|
+
print(f"opened a browser window on the SDK's profile at {url}")
|
|
808
|
+
print(f"profile : {bp.profile_dir()}")
|
|
809
|
+
print()
|
|
810
|
+
print(" 1. sign in the way you normally would (password, SSO, MFA — all of it)")
|
|
811
|
+
print(" 2. close the window when the page you want is visible")
|
|
812
|
+
print(f" 3. check it worked: agentos report check {url}")
|
|
813
|
+
print()
|
|
814
|
+
print(" Tick 'remember me' / 'stay signed in' where the site offers it — a session")
|
|
815
|
+
print(" cookie with no expiry is dropped when the browser closes, by design.")
|
|
816
|
+
return
|
|
817
|
+
|
|
818
|
+
if verb == "status":
|
|
819
|
+
binary = bp.find_browser()
|
|
820
|
+
print(f"browser : {binary or 'NONE FOUND — install Chrome, Chromium, Edge or Brave'}")
|
|
821
|
+
print(f"profile : {bp.profile_dir()}"
|
|
822
|
+
f"{'' if bp.profile_dir().exists() else ' (not created yet)'}")
|
|
823
|
+
pid = bp.busy_pid()
|
|
824
|
+
if pid is not None:
|
|
825
|
+
print(f"in use : yes — a window is open (pid {pid}); headless reads will fail")
|
|
826
|
+
hosts = bp.logged_in_hosts()
|
|
827
|
+
if not hosts:
|
|
828
|
+
print("signed in to: nothing yet — `agentos browser login <url>`")
|
|
829
|
+
else:
|
|
830
|
+
print("signed in to:")
|
|
831
|
+
for host, when in hosts.items():
|
|
832
|
+
print(f" {host:<40} (window opened {when})")
|
|
833
|
+
print()
|
|
834
|
+
print(" A session can expire, and opening a window is not proof of signing in.")
|
|
835
|
+
print(" `agentos report check <url>` is what actually answers 'can I read it'.")
|
|
836
|
+
return
|
|
837
|
+
|
|
838
|
+
if verb == "logout":
|
|
839
|
+
if "--yes" not in rest:
|
|
840
|
+
print(f"This deletes {bp.profile_dir()} and every session signed in to it.")
|
|
841
|
+
print("Re-run with --yes to confirm: agentos browser logout --yes")
|
|
842
|
+
sys.exit(1)
|
|
843
|
+
ok, message = bp.forget()
|
|
844
|
+
print(message if ok else f"error: {message}")
|
|
845
|
+
sys.exit(0 if ok else 1)
|
|
846
|
+
|
|
847
|
+
_browser_usage()
|
|
848
|
+
sys.exit(1)
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _report_check(url: str) -> None:
|
|
852
|
+
"""``agentos report check <URL>`` — can this machine open that link?
|
|
853
|
+
|
|
854
|
+
The diagnostic for the whole point of `sdk.url_access`: a proof link is only
|
|
855
|
+
evidence if something here can read it, and when one comes back unverifiable
|
|
856
|
+
the operator needs to know WHICH door was shut — no `gh` login, a browser
|
|
857
|
+
signed out, a host nobody configured a token for — without filing a report to
|
|
858
|
+
find out. Prints the winning route and a short excerpt, or the reason.
|
|
859
|
+
"""
|
|
860
|
+
import asyncio
|
|
861
|
+
|
|
862
|
+
from sdk import url_access
|
|
863
|
+
|
|
864
|
+
res = asyncio.run(url_access.resolve_url(url))
|
|
865
|
+
print(f"url : {url}")
|
|
866
|
+
print(f"tried : {' -> '.join(res.attempts) or 'nothing'}")
|
|
867
|
+
if res.ok:
|
|
868
|
+
excerpt = res.text.strip().splitlines()
|
|
869
|
+
print(f"readable: yes, via {res.via} ({len(res.text)} chars)")
|
|
870
|
+
print("excerpt :")
|
|
871
|
+
for line in excerpt[:8]:
|
|
872
|
+
print(f" {line[:110]}")
|
|
873
|
+
if len(excerpt) > 8:
|
|
874
|
+
print(f" … {len(excerpt) - 8} more line(s)")
|
|
875
|
+
return
|
|
876
|
+
print("readable: no")
|
|
877
|
+
print(f"reason : it {res.reason}")
|
|
878
|
+
print()
|
|
879
|
+
print(" Fixes, in the order they usually work:")
|
|
880
|
+
print(" · `gh auth login` — private GitHub PRs and issues")
|
|
881
|
+
print(" · `az login` — Outlook/SharePoint via Microsoft Graph")
|
|
882
|
+
print(f" · `agentos browser login {url}` — anything else: sign in once, in a")
|
|
883
|
+
print(" browser profile the SDK keeps, and every later read is signed in too")
|
|
884
|
+
print(f" · add a token for the host to {url_access.AUTH_FILE}")
|
|
885
|
+
sys.exit(1)
|
|
886
|
+
|
|
887
|
+
|
|
761
888
|
def _report_command(args: list[str]) -> None:
|
|
762
889
|
"""``agentos report submit|list|show`` — the CLI half of self-reporting.
|
|
763
890
|
|
|
@@ -785,6 +912,9 @@ def _report_command(args: list[str]) -> None:
|
|
|
785
912
|
if verb == "formats":
|
|
786
913
|
_report_formats()
|
|
787
914
|
return
|
|
915
|
+
if verb == "check" and rest and not rest[0].startswith("-"):
|
|
916
|
+
_report_check(rest[0])
|
|
917
|
+
return
|
|
788
918
|
except TokenNotConfigured as exc:
|
|
789
919
|
# The same failure every other verb gives on an unconfigured machine —
|
|
790
920
|
# `sdk.config` composes a cause-specific message; we only add the prefix.
|
|
@@ -899,8 +1029,20 @@ def _report_formats() -> None:
|
|
|
899
1029
|
print(" ffmpeg is NOT installed — a video proof is recorded and described, but")
|
|
900
1030
|
print(" its content cannot be inspected. Install ffmpeg to change that.")
|
|
901
1031
|
print(" Audio cannot be transcribed locally; it is recorded and described.")
|
|
902
|
-
print(
|
|
903
|
-
print("
|
|
1032
|
+
print()
|
|
1033
|
+
print(" Any other file type is still ACCEPTED — it is attached, named and sized,")
|
|
1034
|
+
print(" and scored as evidence that exists but could not be read. Nothing is")
|
|
1035
|
+
print(" refused for its type; only for size or count.")
|
|
1036
|
+
print()
|
|
1037
|
+
from sdk.self_report import MAX_PROOFS, proof_max_mb
|
|
1038
|
+
print(f" Limits: {proof_max_mb()} MB per proof (AGENTOS_PROOF_MAX_MB), "
|
|
1039
|
+
f"{MAX_PROOFS} proofs per submission.")
|
|
1040
|
+
print()
|
|
1041
|
+
print(" URL proofs are read HERE, with your credentials — a host token, your `gh`")
|
|
1042
|
+
print(" or `az` login, or a browser profile you signed in to once")
|
|
1043
|
+
print(" (`agentos browser login <url>`). `agentos report check <url>` says which.")
|
|
1044
|
+
print(" Media AT a URL is not downloaded as media — attach it as a file to have")
|
|
1045
|
+
print(" it read.")
|
|
904
1046
|
|
|
905
1047
|
|
|
906
1048
|
def _report_list(limit: int) -> None:
|