openhands-agent-server 1.46.0__tar.gz → 1.48.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.
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/PKG-INFO +2 -2
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/api.py +4 -1
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/bash_router.py +14 -3
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/bash_service.py +18 -2
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/conversation_router.py +26 -3
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/conversation_service.py +69 -7
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/dependencies.py +9 -1
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/docker/Dockerfile +114 -33
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/docker/build.py +2 -6
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/event_router.py +1 -3
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/event_service.py +53 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/file_router.py +27 -9
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/mcp_router.py +14 -42
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/models.py +25 -0
- openhands_agent_server-1.48.0/openhands/agent_server/openai/models.py +133 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/openai/router.py +37 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/openai/service.py +327 -58
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/persistence/models.py +7 -2
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/profiles_router.py +1 -1
- openhands_agent_server-1.48.0/openhands/agent_server/runtime_router.py +107 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/server_details_router.py +4 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/session_protocol.py +10 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/session_socket.py +64 -7
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands_agent_server.egg-info/PKG-INFO +2 -2
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands_agent_server.egg-info/SOURCES.txt +2 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands_agent_server.egg-info/requires.txt +1 -1
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/pyproject.toml +3 -3
- openhands_agent_server-1.46.0/openhands/agent_server/openai/models.py +0 -59
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/__init__.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/__main__.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/_secret_redaction.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/_secrets_exposure.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/agent_profiles_router.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/auth_router.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/canvas_extensions/__init__.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/canvas_extensions/installed.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/canvas_extensions/manifest.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/canvas_extensions_router.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/config.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/conversation_lease.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/credential_binding.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/desktop_router.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/env_parser.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/git_router.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/hooks_router.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/hooks_service.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/init_router.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/llm_router.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/logging_config.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/mcp_oauth_store.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/middleware.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/openai/__init__.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/openapi.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/persistence/__init__.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/persistence/store.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/plugins_router.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/plugins_service.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/provider_connections_router.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/pub_sub.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/py.typed +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/settings_router.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/skills_router.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/skills_service.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/sockets.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/sub_agents_router.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/telemetry/__init__.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/telemetry/factory.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/telemetry/http_exporter.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/telemetry/models.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/telemetry/policy.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/telemetry/posthog_exporter.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/telemetry/sanitizer.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/telemetry/service.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/telemetry/sink.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/telemetry/subscriber.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/telemetry_types.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/tool_preload_service.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/tool_router.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/utils.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/vscode_router.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/vscode_service.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/workspace_router.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/workspaces_router.py +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands_agent_server.egg-info/entry_points.txt +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands_agent_server.egg-info/top_level.txt +0 -0
- {openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhands-agent-server
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.48.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
|
|
@@ -15,7 +15,7 @@ Requires-Dist: openhands-sdk
|
|
|
15
15
|
Requires-Dist: openai<3,>=2.33.0
|
|
16
16
|
Requires-Dist: pydantic>=2
|
|
17
17
|
Requires-Dist: sqlalchemy>=2
|
|
18
|
-
Requires-Dist: uvicorn>=0.
|
|
18
|
+
Requires-Dist: uvicorn>=0.52.4
|
|
19
19
|
Requires-Dist: websockets>=12
|
|
20
20
|
Requires-Dist: wsproto>=1.2.0
|
|
21
21
|
Provides-Extra: posthog
|
{openhands_agent_server-1.46.0 → openhands_agent_server-1.48.0}/openhands/agent_server/api.py
RENAMED
|
@@ -40,7 +40,7 @@ from openhands.agent_server.dependencies import (
|
|
|
40
40
|
)
|
|
41
41
|
from openhands.agent_server.desktop_router import desktop_router
|
|
42
42
|
from openhands.agent_server.event_router import event_router
|
|
43
|
-
from openhands.agent_server.file_router import file_router
|
|
43
|
+
from openhands.agent_server.file_router import file_discovery_router, file_router
|
|
44
44
|
from openhands.agent_server.git_router import git_router
|
|
45
45
|
from openhands.agent_server.hooks_router import hooks_router
|
|
46
46
|
from openhands.agent_server.init_router import (
|
|
@@ -60,6 +60,7 @@ from openhands.agent_server.profiles_router import profiles_router
|
|
|
60
60
|
from openhands.agent_server.provider_connections_router import (
|
|
61
61
|
provider_connections_router,
|
|
62
62
|
)
|
|
63
|
+
from openhands.agent_server.runtime_router import create_runtime_router
|
|
63
64
|
from openhands.agent_server.server_details_router import (
|
|
64
65
|
get_server_info,
|
|
65
66
|
mark_initialization_complete,
|
|
@@ -411,6 +412,8 @@ def _add_api_routes(app: FastAPI) -> None:
|
|
|
411
412
|
]
|
|
412
413
|
|
|
413
414
|
api_router = APIRouter(prefix="/api", dependencies=dependencies)
|
|
415
|
+
api_router.include_router(file_discovery_router)
|
|
416
|
+
api_router.include_router(create_runtime_router())
|
|
414
417
|
api_router.include_router(event_router)
|
|
415
418
|
api_router.include_router(conversation_router)
|
|
416
419
|
api_router.include_router(credential_binding_router)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
from typing import Annotated, Literal, cast
|
|
6
7
|
from uuid import UUID
|
|
7
8
|
|
|
@@ -52,13 +53,11 @@ async def search_bash_events(
|
|
|
52
53
|
] = None,
|
|
53
54
|
limit: Annotated[
|
|
54
55
|
int,
|
|
55
|
-
Query(title="The max number of results in the page", gt=0,
|
|
56
|
+
Query(title="The max number of results in the page", gt=0, le=100),
|
|
56
57
|
] = 100,
|
|
57
58
|
bash_event_service: BashEventService = Depends(get_bash_event_service),
|
|
58
59
|
) -> BashEventPage:
|
|
59
60
|
"""Search / List bash event events"""
|
|
60
|
-
assert limit > 0
|
|
61
|
-
assert limit <= 100
|
|
62
61
|
|
|
63
62
|
return await bash_event_service.search_bash_events(
|
|
64
63
|
kind__eq=kind__eq,
|
|
@@ -104,6 +103,7 @@ async def start_bash_command(
|
|
|
104
103
|
) -> BashCommand:
|
|
105
104
|
"""Execute a bash command in the background"""
|
|
106
105
|
update_last_execution_time()
|
|
106
|
+
_validate_cwd(request, bash_event_service)
|
|
107
107
|
command, _ = await bash_event_service.start_bash_command(request)
|
|
108
108
|
return command
|
|
109
109
|
|
|
@@ -115,6 +115,7 @@ async def execute_bash_command(
|
|
|
115
115
|
) -> BashOutput:
|
|
116
116
|
"""Execute a bash command and wait for a result"""
|
|
117
117
|
update_last_execution_time()
|
|
118
|
+
_validate_cwd(request, bash_event_service)
|
|
118
119
|
command, task = await bash_event_service.start_bash_command(request)
|
|
119
120
|
await task
|
|
120
121
|
page = await bash_event_service.search_bash_events(command_id__eq=command.id)
|
|
@@ -129,3 +130,13 @@ async def clear_all_bash_events(
|
|
|
129
130
|
"""Clear all bash events from storage"""
|
|
130
131
|
count = await bash_event_service.clear_all_events()
|
|
131
132
|
return {"cleared_count": count}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _validate_cwd(request: ExecuteBashRequest, service: BashEventService) -> None:
|
|
136
|
+
if service.default_cwd is not None and request.cwd is not None:
|
|
137
|
+
if (
|
|
138
|
+
not Path(request.cwd)
|
|
139
|
+
.resolve()
|
|
140
|
+
.is_relative_to(Path(service.default_cwd).resolve())
|
|
141
|
+
):
|
|
142
|
+
raise HTTPException(422, "cwd must be inside the conversation workspace")
|
|
@@ -31,6 +31,9 @@ class BashEventService:
|
|
|
31
31
|
will not be visible to the agent."""
|
|
32
32
|
|
|
33
33
|
bash_events_dir: Path = field()
|
|
34
|
+
default_cwd: str | None = None
|
|
35
|
+
_tasks: set[asyncio.Task] = field(default_factory=set, init=False)
|
|
36
|
+
_closed: bool = field(default=False, init=False)
|
|
34
37
|
_pub_sub: PubSub[BashEventBase] = field(
|
|
35
38
|
default_factory=lambda: PubSub[BashEventBase](max_subscribers=50),
|
|
36
39
|
init=False,
|
|
@@ -255,13 +258,17 @@ class BashEventService:
|
|
|
255
258
|
self, request: ExecuteBashRequest
|
|
256
259
|
) -> tuple[BashCommand, asyncio.Task]:
|
|
257
260
|
"""Execute a bash command. The output will be published separately."""
|
|
258
|
-
|
|
261
|
+
if self._closed:
|
|
262
|
+
raise RuntimeError("Bash event service is closed")
|
|
263
|
+
cwd = request.cwd or self.default_cwd
|
|
264
|
+
command = BashCommand(**{**request.model_dump(), "cwd": cwd})
|
|
259
265
|
self._save_event_to_file(command)
|
|
260
266
|
await self._pub_sub(command)
|
|
261
267
|
|
|
262
268
|
# Execute the bash command in a background task
|
|
263
269
|
task = asyncio.create_task(self._execute_bash_command(command))
|
|
264
|
-
|
|
270
|
+
self._tasks.add(task)
|
|
271
|
+
task.add_done_callback(self._tasks.discard)
|
|
265
272
|
return command, task
|
|
266
273
|
|
|
267
274
|
async def _execute_bash_command(self, command: BashCommand) -> None:
|
|
@@ -347,6 +354,10 @@ class BashEventService:
|
|
|
347
354
|
timeout=command.timeout,
|
|
348
355
|
)
|
|
349
356
|
exit_code = process.returncode
|
|
357
|
+
except asyncio.CancelledError:
|
|
358
|
+
self._signal_process_group(process, signal.SIGKILL)
|
|
359
|
+
await process.wait()
|
|
360
|
+
raise
|
|
350
361
|
except TimeoutError:
|
|
351
362
|
# Send SIGTERM to the whole process group so user-installed
|
|
352
363
|
# cleanup traps can run, then escalate to SIGKILL if needed.
|
|
@@ -509,6 +520,11 @@ class BashEventService:
|
|
|
509
520
|
|
|
510
521
|
async def close(self):
|
|
511
522
|
"""Close the bash event service and clean up resources."""
|
|
523
|
+
self._closed = True
|
|
524
|
+
tasks = list(self._tasks)
|
|
525
|
+
for task in tasks:
|
|
526
|
+
task.cancel()
|
|
527
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
512
528
|
await self._pub_sub.close()
|
|
513
529
|
|
|
514
530
|
async def __aenter__(self):
|
|
@@ -31,6 +31,8 @@ from openhands.agent_server.models import (
|
|
|
31
31
|
AskAgentResponse,
|
|
32
32
|
ConversationInfo,
|
|
33
33
|
ConversationPage,
|
|
34
|
+
ConversationRuntimeInfo,
|
|
35
|
+
ConversationRuntimeStatus,
|
|
34
36
|
ConversationSortOrder,
|
|
35
37
|
ForkConversationRequest,
|
|
36
38
|
NavigateConversationRequest,
|
|
@@ -94,7 +96,7 @@ async def search_conversations(
|
|
|
94
96
|
] = None,
|
|
95
97
|
limit: Annotated[
|
|
96
98
|
int,
|
|
97
|
-
Query(title="The max number of results in the page", gt=0,
|
|
99
|
+
Query(title="The max number of results in the page", gt=0, le=100),
|
|
98
100
|
] = 100,
|
|
99
101
|
status: Annotated[
|
|
100
102
|
ConversationExecutionStatus | None,
|
|
@@ -108,8 +110,6 @@ async def search_conversations(
|
|
|
108
110
|
conversation_service: ConversationService = Depends(get_conversation_service),
|
|
109
111
|
) -> ConversationPage:
|
|
110
112
|
"""Search / List conversations"""
|
|
111
|
-
assert limit > 0
|
|
112
|
-
assert limit <= 100
|
|
113
113
|
page = await conversation_service.search_conversations(
|
|
114
114
|
page_id, limit, status, sort_order
|
|
115
115
|
)
|
|
@@ -158,6 +158,29 @@ async def get_conversation(
|
|
|
158
158
|
return conversation
|
|
159
159
|
|
|
160
160
|
|
|
161
|
+
@conversation_router.get("/{conversation_id}/runtime")
|
|
162
|
+
async def get_local_conversation_runtime(
|
|
163
|
+
conversation_id: UUID,
|
|
164
|
+
conversation_service: ConversationService = Depends(get_conversation_service),
|
|
165
|
+
) -> ConversationRuntimeInfo:
|
|
166
|
+
"""Inspect the always-available in-process runtime."""
|
|
167
|
+
if await conversation_service.get_conversation(conversation_id) is None:
|
|
168
|
+
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
|
169
|
+
return ConversationRuntimeInfo(
|
|
170
|
+
runtime_status=ConversationRuntimeStatus.AVAILABLE,
|
|
171
|
+
can_resume=True,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@conversation_router.post("/{conversation_id}/runtime/reprovision")
|
|
176
|
+
async def reprovision_local_conversation_runtime(
|
|
177
|
+
conversation_id: UUID,
|
|
178
|
+
conversation_service: ConversationService = Depends(get_conversation_service),
|
|
179
|
+
) -> ConversationRuntimeInfo:
|
|
180
|
+
"""Return local runtime state; local mode has no infrastructure to provision."""
|
|
181
|
+
return await get_local_conversation_runtime(conversation_id, conversation_service)
|
|
182
|
+
|
|
183
|
+
|
|
161
184
|
@conversation_router.get(
|
|
162
185
|
"/{conversation_id}/agent_final_response",
|
|
163
186
|
responses={404: {"description": "Conversation not found"}},
|
|
@@ -61,7 +61,11 @@ from openhands.sdk.conversation.title_utils import (
|
|
|
61
61
|
extract_message_text,
|
|
62
62
|
generate_title_from_message,
|
|
63
63
|
)
|
|
64
|
-
from openhands.sdk.credential import
|
|
64
|
+
from openhands.sdk.credential import (
|
|
65
|
+
CredentialAuthorizationRejected,
|
|
66
|
+
CredentialBindingError,
|
|
67
|
+
VersionedCredentialBinding,
|
|
68
|
+
)
|
|
65
69
|
from openhands.sdk.event import MessageEvent
|
|
66
70
|
from openhands.sdk.event.conversation_state import ConversationStateUpdateEvent
|
|
67
71
|
from openhands.sdk.git.exceptions import GitCommandError, GitRepositoryError
|
|
@@ -352,9 +356,13 @@ def _resolve_agent_from_profile(
|
|
|
352
356
|
cipher: "Cipher | None",
|
|
353
357
|
mcp_config: "dict[str, MCPServer]",
|
|
354
358
|
acp_skill_sourcing: ACPSkillSourcing = "native",
|
|
355
|
-
) -> "tuple[AgentBase, LaunchedAgentProfile]":
|
|
359
|
+
) -> "tuple[AgentBase, LaunchedAgentProfile, set[str] | None]":
|
|
356
360
|
"""Load and resolve an agent profile by id, returning the built agent + provenance.
|
|
357
361
|
|
|
362
|
+
The third element is the profile's secret allow-list (``None`` = unrestricted)
|
|
363
|
+
— strictly ``secret_refs``, with nothing added back. It is returned rather
|
|
364
|
+
than applied here because the secrets ride the start request, not the agent.
|
|
365
|
+
|
|
358
366
|
Runs synchronously (call via ``asyncio.to_thread`` from async context).
|
|
359
367
|
|
|
360
368
|
Args:
|
|
@@ -449,8 +457,10 @@ def _resolve_agent_from_profile(
|
|
|
449
457
|
launched = LaunchedAgentProfile(
|
|
450
458
|
agent_profile_id=profile.id,
|
|
451
459
|
revision=profile.revision,
|
|
460
|
+
secret_refs=profile.secret_refs,
|
|
452
461
|
)
|
|
453
|
-
|
|
462
|
+
allowed_secrets = None if profile.secret_refs is None else set(profile.secret_refs)
|
|
463
|
+
return agent, launched, allowed_secrets
|
|
454
464
|
|
|
455
465
|
|
|
456
466
|
def _compose_conversation_info(
|
|
@@ -819,6 +829,18 @@ class ConversationService:
|
|
|
819
829
|
if event_services is not None
|
|
820
830
|
else None
|
|
821
831
|
)
|
|
832
|
+
record = self._conversation_records.get(conversation_id)
|
|
833
|
+
stored = (
|
|
834
|
+
event_service.stored
|
|
835
|
+
if event_service is not None
|
|
836
|
+
else (record.stored if record is not None else None)
|
|
837
|
+
)
|
|
838
|
+
if stored is not None and not self._profile_allows_secret(
|
|
839
|
+
stored, secret_name
|
|
840
|
+
):
|
|
841
|
+
raise CredentialAuthorizationRejected(
|
|
842
|
+
"The launched agent profile excludes this credential"
|
|
843
|
+
)
|
|
822
844
|
if event_service is not None and event_service.is_open():
|
|
823
845
|
await event_service.activate_credential_binding(secret_name, binding)
|
|
824
846
|
record = self._conversation_records.get(conversation_id)
|
|
@@ -860,6 +882,11 @@ class ConversationService:
|
|
|
860
882
|
raise first_error
|
|
861
883
|
self._credential_bindings = {}
|
|
862
884
|
|
|
885
|
+
@staticmethod
|
|
886
|
+
def _profile_allows_secret(stored: StoredConversation, name: str) -> bool:
|
|
887
|
+
profile = stored.launched_agent_profile
|
|
888
|
+
return profile is None or profile.allows_secret(name)
|
|
889
|
+
|
|
863
890
|
@staticmethod
|
|
864
891
|
def _is_codex_agent(agent: AgentBase | None) -> bool:
|
|
865
892
|
return isinstance(agent, ACPAgent) and agent.acp_server == "codex"
|
|
@@ -885,9 +912,14 @@ class ConversationService:
|
|
|
885
912
|
# ``_load_persisted_state_sync`` usage elsewhere.
|
|
886
913
|
if agent is None:
|
|
887
914
|
agent = await asyncio.to_thread(self._agent_from_base_state, stored.id)
|
|
888
|
-
bindings =
|
|
915
|
+
bindings = {
|
|
916
|
+
name: binding
|
|
917
|
+
for name, binding in self._credential_bindings.pop(stored.id, {}).items()
|
|
918
|
+
if self._profile_allows_secret(stored, name)
|
|
919
|
+
}
|
|
889
920
|
if (
|
|
890
|
-
CODEX_AUTH_SECRET_NAME
|
|
921
|
+
self._profile_allows_secret(stored, CODEX_AUTH_SECRET_NAME)
|
|
922
|
+
and CODEX_AUTH_SECRET_NAME not in bindings
|
|
891
923
|
and self._is_codex_agent(agent)
|
|
892
924
|
and await self._has_local_codex_credential()
|
|
893
925
|
):
|
|
@@ -1427,6 +1459,23 @@ class ConversationService:
|
|
|
1427
1459
|
):
|
|
1428
1460
|
async with self._conversation_lifecycle(conversation_id):
|
|
1429
1461
|
existing_event_service = self._event_services.get(conversation_id)
|
|
1462
|
+
stored = (
|
|
1463
|
+
existing_event_service.stored
|
|
1464
|
+
if existing_event_service is not None
|
|
1465
|
+
else existing_record.stored
|
|
1466
|
+
if existing_record is not None
|
|
1467
|
+
else None
|
|
1468
|
+
)
|
|
1469
|
+
if stored is not None:
|
|
1470
|
+
request = request.model_copy(
|
|
1471
|
+
update={
|
|
1472
|
+
"secrets": {
|
|
1473
|
+
name: value
|
|
1474
|
+
for name, value in request.secrets.items()
|
|
1475
|
+
if self._profile_allows_secret(stored, name)
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
)
|
|
1430
1479
|
if (
|
|
1431
1480
|
existing_event_service is not None
|
|
1432
1481
|
and existing_event_service.is_open()
|
|
@@ -1595,14 +1644,27 @@ class ConversationService:
|
|
|
1595
1644
|
|
|
1596
1645
|
if request.agent_profile_id is not None:
|
|
1597
1646
|
mcp_config = settings.agent_settings.mcp_config
|
|
1598
|
-
|
|
1647
|
+
(
|
|
1648
|
+
resolved_agent,
|
|
1649
|
+
launched_agent_profile,
|
|
1650
|
+
allowed_secrets,
|
|
1651
|
+
) = await asyncio.to_thread(
|
|
1599
1652
|
_resolve_agent_from_profile,
|
|
1600
1653
|
request.agent_profile_id,
|
|
1601
1654
|
self.cipher,
|
|
1602
1655
|
mcp_config,
|
|
1603
1656
|
acp_skill_sourcing=self.acp_skill_sourcing,
|
|
1604
1657
|
)
|
|
1605
|
-
|
|
1658
|
+
updates: dict[str, Any] = {"agent": resolved_agent}
|
|
1659
|
+
# Enforced here, not client-side: a caller that sends more secrets
|
|
1660
|
+
# than the profile allows must not widen the agent's scope.
|
|
1661
|
+
if allowed_secrets is not None:
|
|
1662
|
+
updates["secrets"] = {
|
|
1663
|
+
name: value
|
|
1664
|
+
for name, value in request.secrets.items()
|
|
1665
|
+
if name in allowed_secrets
|
|
1666
|
+
}
|
|
1667
|
+
request = request.model_copy(update=updates)
|
|
1606
1668
|
|
|
1607
1669
|
# Applied unconditionally: a serialized agent always carries
|
|
1608
1670
|
# ``load_memory`` (model_dump emits defaults), so there is no way to
|
|
@@ -70,7 +70,15 @@ def get_conversation_service(request: Request) -> ConversationService:
|
|
|
70
70
|
return service
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
def get_bash_event_service(request: Request) -> BashEventService:
|
|
73
|
+
async def get_bash_event_service(request: Request) -> BashEventService:
|
|
74
|
+
if "runtime_conversation_id" in request.path_params:
|
|
75
|
+
event_service: EventService = request.state.runtime_event_service
|
|
76
|
+
if event_service.bash_event_service is None:
|
|
77
|
+
event_service.bash_event_service = BashEventService(
|
|
78
|
+
bash_events_dir=event_service.conversation_dir / "bash_events",
|
|
79
|
+
default_cwd=event_service.get_conversation().workspace.working_dir,
|
|
80
|
+
)
|
|
81
|
+
return event_service.bash_event_service
|
|
74
82
|
service = getattr(request.app.state, "bash_event_service", None)
|
|
75
83
|
if service is None:
|
|
76
84
|
raise HTTPException(
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# NOTE: LC_ALL/LANG must be set to C.UTF-8 for libtmux to work correctly with
|
|
4
4
|
# PyInstaller builds. Without proper locale, tmux converts UTF-8 separator
|
|
5
5
|
# characters to underscores, breaking libtmux's format parsing.
|
|
6
|
-
ARG BASE_IMAGE=
|
|
6
|
+
ARG BASE_IMAGE=python-node-runtime
|
|
7
7
|
ARG USERNAME=openhands
|
|
8
8
|
ARG UID=10001
|
|
9
9
|
ARG GID=10001
|
|
@@ -26,6 +26,70 @@ ARG INSTALL_ACP_PROVIDERS=claude-code,codex,gemini-cli
|
|
|
26
26
|
# below is written so that's a one-case addition, not a signature change.
|
|
27
27
|
ARG INSTALL_CAPABILITIES=vscode,browser,docker
|
|
28
28
|
|
|
29
|
+
####################################################################################
|
|
30
|
+
# Canonical Python + Node runtime
|
|
31
|
+
#
|
|
32
|
+
# Start from Debian itself and copy only the language runtimes from their official
|
|
33
|
+
# images. The final stage installs only the shared libraries required by Python
|
|
34
|
+
# and Node; developer and package-manager tooling is added by the agent-server
|
|
35
|
+
# stages below when needed. Package upgrades resolve against a dated snapshot so
|
|
36
|
+
# newly published Debian packages have a seven-day observation period.
|
|
37
|
+
####################################################################################
|
|
38
|
+
FROM python:3.13.15-slim-trixie AS python-runtime
|
|
39
|
+
FROM node:24.21.0-trixie-slim AS node-runtime
|
|
40
|
+
FROM debian:trixie-slim AS python-node-runtime
|
|
41
|
+
# Temporary exception to pick up Debian 13.7 before the normal observation period.
|
|
42
|
+
ARG DEBIAN_SNAPSHOT=20260913T000000Z
|
|
43
|
+
|
|
44
|
+
COPY --from=python-runtime /usr/local /usr/local
|
|
45
|
+
COPY --from=node-runtime /usr/local/bin/node /usr/local/bin/node
|
|
46
|
+
COPY --from=node-runtime /usr/local/lib/node_modules /usr/local/lib/node_modules
|
|
47
|
+
|
|
48
|
+
RUN set -eux; \
|
|
49
|
+
ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm; \
|
|
50
|
+
ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx; \
|
|
51
|
+
ln -s ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack; \
|
|
52
|
+
printf '%s\n' \
|
|
53
|
+
'Types: deb' \
|
|
54
|
+
"URIs: http://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}/" \
|
|
55
|
+
'Suites: trixie' \
|
|
56
|
+
'Components: main' \
|
|
57
|
+
'Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg' \
|
|
58
|
+
'Check-Valid-Until: no' \
|
|
59
|
+
'' \
|
|
60
|
+
'Types: deb' \
|
|
61
|
+
"URIs: http://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}/" \
|
|
62
|
+
'Suites: trixie-security' \
|
|
63
|
+
'Components: main' \
|
|
64
|
+
'Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg' \
|
|
65
|
+
'Check-Valid-Until: no' \
|
|
66
|
+
> /etc/apt/sources.list.d/debian.sources; \
|
|
67
|
+
apt-get update; \
|
|
68
|
+
apt-get upgrade -y; \
|
|
69
|
+
apt-get install -y --no-install-recommends \
|
|
70
|
+
ca-certificates \
|
|
71
|
+
libbz2-1.0 \
|
|
72
|
+
libdb5.3t64 \
|
|
73
|
+
libffi8 \
|
|
74
|
+
libgcc-s1 \
|
|
75
|
+
libgdbm6t64 \
|
|
76
|
+
liblzma5 \
|
|
77
|
+
libncursesw6 \
|
|
78
|
+
libreadline8t64 \
|
|
79
|
+
libsqlite3-0 \
|
|
80
|
+
libssl3t64 \
|
|
81
|
+
libstdc++6 \
|
|
82
|
+
libtinfo6 \
|
|
83
|
+
libuuid1 \
|
|
84
|
+
libzstd1 \
|
|
85
|
+
netbase \
|
|
86
|
+
tzdata \
|
|
87
|
+
zlib1g; \
|
|
88
|
+
rm -rf /var/lib/apt/lists/*; \
|
|
89
|
+
python3 --version; \
|
|
90
|
+
node --version; \
|
|
91
|
+
npm --version
|
|
92
|
+
|
|
29
93
|
####################################################################################
|
|
30
94
|
# Builder (source mode)
|
|
31
95
|
# We copy source + build a venv here for local dev and debugging.
|
|
@@ -168,9 +232,8 @@ RUN set -eux; \
|
|
|
168
232
|
|
|
169
233
|
####################################################################################
|
|
170
234
|
# Base image (minimal)
|
|
171
|
-
# It includes only
|
|
172
|
-
#
|
|
173
|
-
# Suitable for running in headless/evaluation mode.
|
|
235
|
+
# It includes only the packages required to run the agent server and its Bash
|
|
236
|
+
# and Git APIs. Development tools and optional capabilities live in base-image.
|
|
174
237
|
####################################################################################
|
|
175
238
|
FROM ${BASE_IMAGE} AS base-image-minimal
|
|
176
239
|
ARG USERNAME UID GID PORT
|
|
@@ -192,36 +255,22 @@ RUN set -eux; \
|
|
|
192
255
|
if command -v apt-get >/dev/null 2>&1; then \
|
|
193
256
|
apt-get -o Acquire::Retries=5 update; \
|
|
194
257
|
apt-get -o Acquire::Retries=5 install -y --no-install-recommends \
|
|
195
|
-
bash ca-certificates
|
|
196
|
-
build-essential coreutils util-linux procps findutils grep sed \
|
|
197
|
-
tini apt-transport-https gnupg lsb-release xz-utils; \
|
|
258
|
+
bash ca-certificates git tini tmux; \
|
|
198
259
|
rm -rf /var/lib/apt/lists/*; \
|
|
199
260
|
elif command -v apk >/dev/null 2>&1; then \
|
|
200
|
-
apk add --no-cache \
|
|
201
|
-
bash ca-certificates curl wget sudo git jq tmux tar build-base \
|
|
202
|
-
coreutils util-linux procps findutils grep sed tini gnupg shadow xz; \
|
|
261
|
+
apk add --no-cache bash ca-certificates git shadow tini tmux; \
|
|
203
262
|
elif command -v microdnf >/dev/null 2>&1; then \
|
|
204
|
-
microdnf install -y \
|
|
205
|
-
bash ca-certificates curl wget sudo git jq tmux tar make gcc gcc-c++ \
|
|
206
|
-
coreutils util-linux procps-ng findutils grep sed shadow-utils \
|
|
207
|
-
gnupg2 xz; \
|
|
263
|
+
microdnf install -y bash ca-certificates git shadow-utils tmux; \
|
|
208
264
|
microdnf clean all; \
|
|
209
265
|
elif command -v dnf >/dev/null 2>&1; then \
|
|
210
|
-
dnf install -y \
|
|
211
|
-
bash ca-certificates curl wget sudo git jq tmux tar make gcc gcc-c++ \
|
|
212
|
-
coreutils util-linux procps-ng findutils grep sed shadow-utils \
|
|
213
|
-
gnupg2 xz; \
|
|
266
|
+
dnf install -y bash ca-certificates git shadow-utils tmux; \
|
|
214
267
|
dnf clean all; \
|
|
215
268
|
elif command -v yum >/dev/null 2>&1; then \
|
|
216
|
-
yum install -y \
|
|
217
|
-
bash ca-certificates curl wget sudo git jq tmux tar make gcc gcc-c++ \
|
|
218
|
-
coreutils util-linux procps-ng findutils grep sed shadow-utils \
|
|
219
|
-
gnupg2 xz; \
|
|
269
|
+
yum install -y bash ca-certificates git shadow-utils tmux; \
|
|
220
270
|
yum clean all; \
|
|
221
271
|
elif command -v zypper >/dev/null 2>&1; then \
|
|
222
272
|
zypper --non-interactive install --no-recommends \
|
|
223
|
-
bash ca-certificates
|
|
224
|
-
coreutils util-linux procps findutils grep sed shadow gpg2 xz; \
|
|
273
|
+
bash ca-certificates git shadow tmux; \
|
|
225
274
|
zypper clean --all; \
|
|
226
275
|
else \
|
|
227
276
|
echo "Unsupported base image: no known package manager found" >&2; \
|
|
@@ -230,11 +279,6 @@ RUN set -eux; \
|
|
|
230
279
|
grep -Eq "^[^:]*:[^:]*:${GID}:" /etc/group || groupadd -g "${GID}" "${USERNAME}"; \
|
|
231
280
|
grep -Eq "^${USERNAME}:" /etc/passwd || \
|
|
232
281
|
useradd -m -u "${UID}" -g "${GID}" -s /bin/bash "${USERNAME}"; \
|
|
233
|
-
# Best-effort: add user to a sudo group when one exists (Debian-style
|
|
234
|
-
# `sudo` group). On Alpine/RHEL/SUSE there is no `sudo` group by default,
|
|
235
|
-
# and the NOPASSWD sudoers line below grants sudo regardless of group.
|
|
236
|
-
usermod -aG sudo "${USERNAME}" 2>/dev/null || true; \
|
|
237
|
-
echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers; \
|
|
238
282
|
mkdir -p /workspace/project; \
|
|
239
283
|
chown -R "${USERNAME}:${USERNAME}" /workspace
|
|
240
284
|
|
|
@@ -274,10 +318,6 @@ RUN if ! "$ACP_NODE_DIR/bin/node" --version >/dev/null 2>&1; then \
|
|
|
274
318
|
RUN mkdir -p /etc/claude-code && \
|
|
275
319
|
echo '{"permissions":{"allow":["Edit","Read","Bash"]}}' > /etc/claude-code/managed-settings.json
|
|
276
320
|
|
|
277
|
-
# NOTE: we should NOT include UV_PROJECT_ENVIRONMENT here,
|
|
278
|
-
# since the agent might use it to perform other work (e.g. tools that use Python)
|
|
279
|
-
COPY --from=ghcr.io/astral-sh/uv:0.11.6 /uv /uvx /bin/
|
|
280
|
-
|
|
281
321
|
USER ${USERNAME}
|
|
282
322
|
WORKDIR /
|
|
283
323
|
# Locale settings required for libtmux to work with PyInstaller builds
|
|
@@ -299,6 +339,47 @@ ARG INSTALL_CAPABILITIES
|
|
|
299
339
|
|
|
300
340
|
USER root
|
|
301
341
|
|
|
342
|
+
# Full images are general-purpose development environments. Keep compilers,
|
|
343
|
+
# package/repository helpers, and command-line utilities out of binary-minimal.
|
|
344
|
+
RUN set -eux; \
|
|
345
|
+
if command -v apt-get >/dev/null 2>&1; then \
|
|
346
|
+
apt-get -o Acquire::Retries=5 update; \
|
|
347
|
+
apt-get -o Acquire::Retries=5 install -y --no-install-recommends \
|
|
348
|
+
apt-transport-https apt-utils build-essential coreutils curl \
|
|
349
|
+
findutils gnupg grep jq lsb-release procps sed sudo tar util-linux \
|
|
350
|
+
wget xz-utils; \
|
|
351
|
+
rm -rf /var/lib/apt/lists/*; \
|
|
352
|
+
elif command -v apk >/dev/null 2>&1; then \
|
|
353
|
+
apk add --no-cache \
|
|
354
|
+
build-base coreutils curl findutils gnupg grep jq procps sudo tar \
|
|
355
|
+
util-linux wget xz; \
|
|
356
|
+
elif command -v microdnf >/dev/null 2>&1; then \
|
|
357
|
+
microdnf install -y \
|
|
358
|
+
coreutils curl findutils gcc gcc-c++ gnupg2 grep jq make procps-ng \
|
|
359
|
+
sed sudo tar util-linux wget xz; \
|
|
360
|
+
microdnf clean all; \
|
|
361
|
+
elif command -v dnf >/dev/null 2>&1; then \
|
|
362
|
+
dnf install -y \
|
|
363
|
+
coreutils curl findutils gcc gcc-c++ gnupg2 grep jq make procps-ng \
|
|
364
|
+
sed sudo tar util-linux wget xz; \
|
|
365
|
+
dnf clean all; \
|
|
366
|
+
elif command -v yum >/dev/null 2>&1; then \
|
|
367
|
+
yum install -y \
|
|
368
|
+
coreutils curl findutils gcc gcc-c++ gnupg2 grep jq make procps-ng \
|
|
369
|
+
sed sudo tar util-linux wget xz; \
|
|
370
|
+
yum clean all; \
|
|
371
|
+
elif command -v zypper >/dev/null 2>&1; then \
|
|
372
|
+
zypper --non-interactive install --no-recommends \
|
|
373
|
+
coreutils curl findutils gcc gcc-c++ gpg2 grep jq make procps sed \
|
|
374
|
+
sudo tar util-linux wget xz; \
|
|
375
|
+
zypper clean --all; \
|
|
376
|
+
fi; \
|
|
377
|
+
usermod -aG sudo "${USERNAME}" 2>/dev/null || true; \
|
|
378
|
+
echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
|
379
|
+
|
|
380
|
+
# Keep uv as part of the full development environment, but not binary-minimal.
|
|
381
|
+
COPY --from=ghcr.io/astral-sh/uv:0.11.6 /uv /uvx /bin/
|
|
382
|
+
|
|
302
383
|
# Fail fast on a typo'd capability name before spending any time on the
|
|
303
384
|
# installs below (each block only checks for ITS OWN keyword's presence, so
|
|
304
385
|
# an unrecognized one would otherwise be silently ignored rather than erroring).
|
|
@@ -393,9 +393,7 @@ _DEFAULT_PACKAGE_VERSION = _package_version()
|
|
|
393
393
|
|
|
394
394
|
|
|
395
395
|
class BuildOptions(BaseModel):
|
|
396
|
-
|
|
397
|
-
# with Python 3.13. See issue #1886 for details.
|
|
398
|
-
base_image: str = Field(default="nikolaik/python-nodejs:python3.12-nodejs22-slim")
|
|
396
|
+
base_image: str = Field(default="python-node-runtime")
|
|
399
397
|
custom_tags: str = Field(
|
|
400
398
|
default="", description="Comma-separated list of custom tags."
|
|
401
399
|
)
|
|
@@ -1069,9 +1067,7 @@ def main(argv: list[str]) -> int:
|
|
|
1069
1067
|
)
|
|
1070
1068
|
parser.add_argument(
|
|
1071
1069
|
"--base-image",
|
|
1072
|
-
|
|
1073
|
-
# with Python 3.13. See issue #1886.
|
|
1074
|
-
default=_env("BASE_IMAGE", "nikolaik/python-nodejs:python3.12-nodejs22-slim"),
|
|
1070
|
+
default=_env("BASE_IMAGE", "python-node-runtime"),
|
|
1075
1071
|
help="Base image to use (default from $BASE_IMAGE).",
|
|
1076
1072
|
)
|
|
1077
1073
|
parser.add_argument(
|
|
@@ -70,7 +70,7 @@ async def search_conversation_events(
|
|
|
70
70
|
] = None,
|
|
71
71
|
limit: Annotated[
|
|
72
72
|
int,
|
|
73
|
-
Query(title="The max number of results in the page", gt=0,
|
|
73
|
+
Query(title="The max number of results in the page", gt=0, le=100),
|
|
74
74
|
] = 100,
|
|
75
75
|
kind: Annotated[
|
|
76
76
|
str | None,
|
|
@@ -101,8 +101,6 @@ async def search_conversation_events(
|
|
|
101
101
|
event_service: EventService = Depends(get_event_service),
|
|
102
102
|
) -> JSONResponse:
|
|
103
103
|
"""Search / List local events"""
|
|
104
|
-
assert limit > 0
|
|
105
|
-
assert limit <= 100
|
|
106
104
|
|
|
107
105
|
# Normalize timezone-aware datetimes to server timezone
|
|
108
106
|
normalized_gte = (
|