openhands-agent-server 1.36.0__tar.gz → 1.37.0__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.
Files changed (81) hide show
  1. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/PKG-INFO +3 -1
  2. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/api.py +85 -0
  3. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/config.py +87 -1
  4. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/conversation_service.py +671 -194
  5. openhands_agent_server-1.37.0/openhands/agent_server/credential_binding.py +107 -0
  6. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/event_router.py +6 -2
  7. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/event_service.py +216 -1
  8. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/init_router.py +27 -1
  9. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/mcp_oauth_store.py +7 -2
  10. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/persistence/models.py +9 -2
  11. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/persistence/store.py +118 -16
  12. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/plugins_router.py +47 -11
  13. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/plugins_service.py +115 -18
  14. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/server_details_router.py +1 -0
  15. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/settings_router.py +7 -0
  16. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/skills_service.py +12 -2
  17. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/sockets.py +1 -3
  18. openhands_agent_server-1.37.0/openhands/agent_server/telemetry/__init__.py +88 -0
  19. openhands_agent_server-1.37.0/openhands/agent_server/telemetry/factory.py +148 -0
  20. openhands_agent_server-1.37.0/openhands/agent_server/telemetry/http_exporter.py +83 -0
  21. openhands_agent_server-1.37.0/openhands/agent_server/telemetry/models.py +315 -0
  22. openhands_agent_server-1.37.0/openhands/agent_server/telemetry/policy.py +216 -0
  23. openhands_agent_server-1.37.0/openhands/agent_server/telemetry/posthog_exporter.py +93 -0
  24. openhands_agent_server-1.37.0/openhands/agent_server/telemetry/sanitizer.py +356 -0
  25. openhands_agent_server-1.37.0/openhands/agent_server/telemetry/service.py +243 -0
  26. openhands_agent_server-1.37.0/openhands/agent_server/telemetry/sink.py +304 -0
  27. openhands_agent_server-1.37.0/openhands/agent_server/telemetry/subscriber.py +305 -0
  28. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands_agent_server.egg-info/PKG-INFO +3 -1
  29. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands_agent_server.egg-info/SOURCES.txt +22 -2
  30. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands_agent_server.egg-info/requires.txt +3 -0
  31. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/pyproject.toml +8 -1
  32. openhands_agent_server-1.36.0/openhands/agent_server/event_compat.py +0 -50
  33. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/__init__.py +0 -0
  34. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/__main__.py +0 -0
  35. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/_secrets_exposure.py +0 -0
  36. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/agent_profiles_router.py +0 -0
  37. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/auth_router.py +0 -0
  38. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/bash_router.py +0 -0
  39. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/bash_service.py +0 -0
  40. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/conversation_lease.py +0 -0
  41. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/conversation_router.py +0 -0
  42. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/dependencies.py +0 -0
  43. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/desktop_router.py +0 -0
  44. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/desktop_service.py +0 -0
  45. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/docker/Dockerfile +0 -0
  46. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/docker/build.py +0 -0
  47. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/docker/wallpaper.svg +0 -0
  48. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/env_parser.py +0 -0
  49. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/file_router.py +0 -0
  50. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/git_router.py +0 -0
  51. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/hooks_router.py +0 -0
  52. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/hooks_service.py +0 -0
  53. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/llm_router.py +0 -0
  54. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/logging_config.py +0 -0
  55. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/mcp_router.py +0 -0
  56. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/middleware.py +0 -0
  57. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/models.py +0 -0
  58. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/openai/__init__.py +0 -0
  59. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/openai/models.py +0 -0
  60. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/openai/router.py +0 -0
  61. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/openai/service.py +0 -0
  62. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/openapi.py +0 -0
  63. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/persistence/__init__.py +0 -0
  64. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/profiles_router.py +0 -0
  65. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/pub_sub.py +0 -0
  66. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/py.typed +0 -0
  67. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/skills_router.py +0 -0
  68. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/sub_agents_router.py +0 -0
  69. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/tool_preload_service.py +0 -0
  70. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/tool_router.py +0 -0
  71. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/utils.py +0 -0
  72. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
  73. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
  74. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/vscode_router.py +0 -0
  75. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/vscode_service.py +0 -0
  76. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/workspace_router.py +0 -0
  77. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands/agent_server/workspaces_router.py +0 -0
  78. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
  79. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands_agent_server.egg-info/entry_points.txt +0 -0
  80. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/openhands_agent_server.egg-info/top_level.txt +0 -0
  81. {openhands_agent_server-1.36.0 → openhands_agent_server-1.37.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-agent-server
3
- Version: 1.36.0
3
+ Version: 1.37.0
4
4
  Summary: OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -18,3 +18,5 @@ Requires-Dist: sqlalchemy>=2
18
18
  Requires-Dist: uvicorn>=0.31.1
19
19
  Requires-Dist: websockets>=12
20
20
  Requires-Dist: wsproto>=1.2.0
21
+ Provides-Extra: posthog
22
+ Requires-Dist: posthog<7,>=6; extra == "posthog"
@@ -28,6 +28,9 @@ from openhands.agent_server.conversation_router import conversation_router
28
28
  from openhands.agent_server.conversation_service import (
29
29
  get_default_conversation_service,
30
30
  )
31
+ from openhands.agent_server.credential_binding import (
32
+ router as credential_binding_router,
33
+ )
31
34
  from openhands.agent_server.dependencies import (
32
35
  check_session_api_key,
33
36
  check_workspace_session,
@@ -61,6 +64,23 @@ from openhands.agent_server.settings_router import settings_router
61
64
  from openhands.agent_server.skills_router import skills_router
62
65
  from openhands.agent_server.sockets import sockets_router
63
66
  from openhands.agent_server.sub_agents_router import sub_agents_router
67
+ from openhands.agent_server.telemetry import (
68
+ build_telemetry_sink,
69
+ emit_server_started,
70
+ emit_server_stopped,
71
+ get_event_factory,
72
+ get_telemetry_sink,
73
+ shutdown_telemetry_sink,
74
+ )
75
+ from openhands.agent_server.telemetry.factory import (
76
+ DISTINCT_ID_HEADER,
77
+ distinct_id_from_header,
78
+ )
79
+ from openhands.agent_server.telemetry.models import (
80
+ EventName,
81
+ RequestFailedProperties,
82
+ )
83
+ from openhands.agent_server.telemetry.sanitizer import normalize_exception
64
84
  from openhands.agent_server.tool_preload_service import get_tool_preload_service
65
85
  from openhands.agent_server.tool_router import tool_router
66
86
  from openhands.agent_server.vscode_router import vscode_router
@@ -133,6 +153,13 @@ async def api_lifespan(api: FastAPI) -> AsyncIterator[None]:
133
153
 
134
154
  config: Config = api.state.config
135
155
  deferred = config.deferred_init
156
+
157
+ # Deferred pods boot with telemetry disabled and are rebuilt by
158
+ # InitService, so they emit `server_started` there instead.
159
+ api.state.telemetry_sink = await build_telemetry_sink(config)
160
+ if not deferred:
161
+ emit_server_started()
162
+
136
163
  vscode_service = get_vscode_service()
137
164
  desktop_service = get_desktop_service()
138
165
  tool_preload_service = get_tool_preload_service()
@@ -265,12 +292,67 @@ async def api_lifespan(api: FastAPI) -> AsyncIterator[None]:
265
292
 
266
293
  await stop_stateless_services()
267
294
  finally:
295
+ # Outer finally so a startup failure cannot leak the drain task, and
296
+ # after `async with service` so terminal events are still accepted.
297
+ emit_server_stopped()
298
+ await shutdown_telemetry_sink()
299
+
268
300
  if tmux_tmpdir_was_defaulted and os.environ.get("TMUX_TMPDIR") == str(
269
301
  tmux_tmpdir
270
302
  ):
271
303
  os.environ.pop("TMUX_TMPDIR", None)
272
304
 
273
305
 
306
+ def _emit_request_failed(request: Request, exc: Exception, error_id: str) -> None:
307
+ """Report an unhandled 5xx as a sanitized diagnostic event.
308
+
309
+ Sends the *route template* (``/api/conversations/{conversation_id}``)
310
+ rather than ``request.url.path``, which embeds real identifiers. Fully
311
+ defensive: an error in the telemetry path must not replace the 500 the
312
+ caller is already getting with a different failure.
313
+ """
314
+ try:
315
+ sink = get_telemetry_sink()
316
+ if not sink.enabled:
317
+ return
318
+
319
+ factory = get_event_factory()
320
+ if factory is None:
321
+ return
322
+
323
+ route = request.scope.get("route")
324
+ route_template = getattr(route, "path", None)
325
+ if not isinstance(route_template, str) or not route_template:
326
+ # Unmatched route: reporting the raw path could leak identifiers.
327
+ route_template = "/unmatched"
328
+
329
+ method = request.method.upper()
330
+ if method not in ("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"):
331
+ return
332
+
333
+ fingerprint = normalize_exception(exc)
334
+ # Attribute to the frontend's analytics identity when it supplied one;
335
+ # request-scoped activity has no conversation user_id otherwise.
336
+ distinct_id = distinct_id_from_header(request.headers.get(DISTINCT_ID_HEADER))
337
+ sink.emit(
338
+ factory.build(
339
+ EventName.REQUEST_FAILED,
340
+ RequestFailedProperties(
341
+ route_template=route_template,
342
+ method=method, # type: ignore[arg-type]
343
+ status_code=500,
344
+ error_class=fingerprint.error_class,
345
+ error_category=fingerprint.error_category,
346
+ error_fingerprint=fingerprint.error_fingerprint,
347
+ error_id=error_id,
348
+ ),
349
+ user_id=distinct_id,
350
+ )
351
+ )
352
+ except Exception:
353
+ logger.debug("Could not emit request_failed telemetry", exc_info=True)
354
+
355
+
274
356
  def _get_root_path(config: Config) -> str:
275
357
  root_path = ""
276
358
  if config.web_url:
@@ -343,6 +425,7 @@ def _add_api_routes(app: FastAPI) -> None:
343
425
  api_router = APIRouter(prefix="/api", dependencies=dependencies)
344
426
  api_router.include_router(event_router)
345
427
  api_router.include_router(conversation_router)
428
+ api_router.include_router(credential_binding_router)
346
429
  api_router.include_router(tool_router)
347
430
  api_router.include_router(bash_router)
348
431
  api_router.include_router(git_router)
@@ -517,6 +600,7 @@ def _add_exception_handlers(api: FastAPI) -> None:
517
600
  error_id,
518
601
  exc_info=(type(exc), exc, exc.__traceback__),
519
602
  )
603
+ _emit_request_failed(request, exc, error_id)
520
604
  return JSONResponse(status_code=500, content=content)
521
605
 
522
606
  # Logs full stack trace for any unhandled error that FastAPI would
@@ -528,6 +612,7 @@ def _add_exception_handlers(api: FastAPI) -> None:
528
612
  error_id,
529
613
  exc_info=(type(exc), exc, exc.__traceback__),
530
614
  )
615
+ _emit_request_failed(request, exc, error_id)
531
616
  return JSONResponse(status_code=500, content=content)
532
617
 
533
618
  @api.exception_handler(HTTPException)
@@ -2,7 +2,7 @@ import json
2
2
  import logging
3
3
  import os
4
4
  from pathlib import Path
5
- from typing import Any, ClassVar
5
+ from typing import Any, ClassVar, Literal
6
6
 
7
7
  from pydantic import BaseModel, ConfigDict, Field, SecretStr
8
8
 
@@ -111,6 +111,85 @@ class WebhookSpec(BaseModel):
111
111
  )
112
112
 
113
113
 
114
+ TelemetryExporterKind = Literal["none", "posthog", "http"]
115
+ """Which exporter ships diagnostic events, if any."""
116
+
117
+
118
+ class TelemetrySpec(BaseModel):
119
+ """Deployment-supplied product-analytics transport settings.
120
+
121
+ This carries *transport* only. Whether telemetry may be delivered is
122
+ resolved from consent (``misc_settings.telemetry.consent``, optionally
123
+ seeded or overridden by ``OH_TELEMETRY_CONSENT``) — there is no deployment
124
+ "mode" here, and nothing in the agent-server special-cases a hosted
125
+ deployment.
126
+ """
127
+
128
+ exporter: TelemetryExporterKind = Field(
129
+ default="none",
130
+ description=(
131
+ "Exporter to use. 'none' (the default) never delivers, and is what "
132
+ "library and headless consumers get. 'posthog' requires the "
133
+ "[posthog] extra. 'http' POSTs sanitized batches to "
134
+ "telemetry_http_endpoint."
135
+ ),
136
+ )
137
+ posthog_api_key: SecretStr | None = Field(
138
+ default=None,
139
+ description=(
140
+ "PostHog project API key. Required by the 'posthog' exporter; "
141
+ "without it telemetry stays inactive."
142
+ ),
143
+ )
144
+ posthog_host: str = Field(
145
+ default="https://us.i.posthog.com",
146
+ description="PostHog ingestion host.",
147
+ )
148
+ http_endpoint: str | None = Field(
149
+ default=None,
150
+ description=(
151
+ "Endpoint the 'http' exporter POSTs sanitized event batches to. "
152
+ "Intended to front a backend that revalidates auth and consent "
153
+ "before forwarding onward."
154
+ ),
155
+ )
156
+ http_token: SecretStr | None = Field(
157
+ default=None,
158
+ description="Bearer token sent by the 'http' exporter, if required.",
159
+ )
160
+ salt: SecretStr | None = Field(
161
+ default=None,
162
+ description=(
163
+ "Key used to pseudonymize conversation identifiers. Falls back to "
164
+ "a per-process random salt, which keeps pseudonyms stable within a "
165
+ "run but unlinkable across runs."
166
+ ),
167
+ )
168
+ max_queue_size: int = Field(
169
+ default=1000,
170
+ ge=1,
171
+ description=(
172
+ "Upper bound on buffered diagnostic events. The queue is bounded "
173
+ "on ingest: past this many events the oldest are dropped, so a "
174
+ "failing exporter cannot grow memory without limit."
175
+ ),
176
+ )
177
+ event_buffer_size: int = Field(
178
+ default=20, ge=1, description="Maximum events per delivery batch."
179
+ )
180
+ flush_delay: float = Field(
181
+ default=30.0,
182
+ gt=0,
183
+ description="Seconds to wait before flushing a partial batch.",
184
+ )
185
+ num_retries: int = Field(
186
+ default=2, ge=0, description="Retries before a batch is dropped."
187
+ )
188
+ retry_delay: float = Field(
189
+ default=5.0, ge=0, description="Base seconds between delivery retries."
190
+ )
191
+
192
+
114
193
  class Config(BaseModel):
115
194
  """
116
195
  Immutable configuration for a server running in local mode.
@@ -270,6 +349,13 @@ class Config(BaseModel):
270
349
  "to expire before the first renewal, effectively making it one-shot."
271
350
  ),
272
351
  )
352
+ telemetry: TelemetrySpec = Field(
353
+ default_factory=TelemetrySpec,
354
+ description=(
355
+ "Product-analytics policy. Disabled by default; see TelemetrySpec. "
356
+ "Distinct from LLM completion logging and from Laminar/OTel tracing."
357
+ ),
358
+ )
273
359
  model_config: ClassVar[ConfigDict] = {"frozen": True}
274
360
 
275
361
  @property