mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-23 23:32:22 +02:00
feat(agent): expose chat history with existing chat scope (#6443)
Co-authored-by: Raul <9117159+raultcj@users.noreply.github.com>
This commit is contained in:
@@ -755,7 +755,8 @@ app.include_router(setup_research_routes(research_handler, session_manager=sessi
|
||||
|
||||
# History
|
||||
from routes.history.history_routes import setup_history_routes
|
||||
app.include_router(setup_history_routes(session_manager, upload_handler=upload_handler))
|
||||
history_router = setup_history_routes(session_manager, upload_handler=upload_handler)
|
||||
app.include_router(history_router)
|
||||
|
||||
# Search
|
||||
from routes.search.search_routes import setup_search_routes
|
||||
@@ -915,6 +916,7 @@ app.include_router(setup_codex_routes(
|
||||
memory_router=memory_router,
|
||||
calendar_router=calendar_router,
|
||||
document_router=document_router,
|
||||
history_router=history_router,
|
||||
))
|
||||
app.include_router(setup_claude_routes())
|
||||
|
||||
|
||||
@@ -100,6 +100,14 @@ python3 ~/.claude/skills/odysseus/scripts/odysseus_api.py POST /api/codex/memory
|
||||
- `POST /api/codex/documents` — body `{"session_id": "...", "title": "...", "content": "...", "language": "markdown"}`. Requires `documents:write`.
|
||||
- `DELETE /api/codex/documents/{doc_id}` — delete a document. Requires `documents:write`.
|
||||
|
||||
## Chat history
|
||||
|
||||
- `GET /api/codex/chat/{session_id}?offset=0&limit=100` — read a paginated conversation transcript owned by the token owner. Requires the existing `chat` scope.
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/odysseus/scripts/odysseus_api.py GET /api/codex/chat/SESSION_ID
|
||||
```
|
||||
|
||||
## Email draft + send
|
||||
|
||||
- Prefer `POST /api/codex/emails/draft-document` for agent-written email replies. It creates an editable Odysseus Document with `language: "email"` and does not touch IMAP/send.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Small Odysseus scoped API helper for Codex terminal sessions."""
|
||||
"""Small Odysseus scoped API helper for Claude Code terminal sessions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -44,7 +44,7 @@ def _config() -> tuple[str, str] | None:
|
||||
if not token:
|
||||
missing.append("ODYSSEUS_API_TOKEN")
|
||||
if missing:
|
||||
print(f"missing {', '.join(missing)}; create a Codex Agent token in Odysseus Settings", file=sys.stderr)
|
||||
print(f"missing {', '.join(missing)}; create a Claude Agent token in Odysseus Settings", file=sys.stderr)
|
||||
return None
|
||||
return base_url, token
|
||||
|
||||
|
||||
@@ -100,6 +100,14 @@ python3 integrations/codex/scripts/odysseus_api.py POST /api/codex/memory '{"tex
|
||||
- `POST /api/codex/documents` — body `{"session_id": "...", "title": "...", "content": "...", "language": "markdown"}`. Requires `documents:write`.
|
||||
- `DELETE /api/codex/documents/{doc_id}` — delete a document. Requires `documents:write`.
|
||||
|
||||
## Chat history
|
||||
|
||||
- `GET /api/codex/chat/{session_id}?offset=0&limit=100` — read a paginated conversation transcript owned by the token owner. Requires the existing `chat` scope.
|
||||
|
||||
```bash
|
||||
python3 integrations/codex/scripts/odysseus_api.py GET /api/codex/chat/SESSION_ID
|
||||
```
|
||||
|
||||
## Email draft + send
|
||||
|
||||
- Prefer `POST /api/codex/emails/draft-document` for Codex-written email replies. It creates an editable Odysseus Document with `language: "email"` and does not touch IMAP/send.
|
||||
|
||||
@@ -35,6 +35,7 @@ CALENDAR_READ_SCOPES = {"calendar:read", "calendar:write"}
|
||||
CALENDAR_WRITE_SCOPES = {"calendar:write"}
|
||||
DOCS_READ_SCOPES = {"documents:read", "documents:write"}
|
||||
DOCS_WRITE_SCOPES = {"documents:write"}
|
||||
CHAT_READ_SCOPES = {"chat"}
|
||||
WRITE_ACTIONS = {"add", "create", "new", "save", "remind", "update", "delete", "toggle_item", "remove", "remove_item"}
|
||||
|
||||
|
||||
@@ -161,6 +162,7 @@ def setup_codex_routes(
|
||||
memory_router: APIRouter | None = None,
|
||||
calendar_router: APIRouter | None = None,
|
||||
document_router: APIRouter | None = None,
|
||||
history_router: APIRouter | None = None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/codex", tags=["codex"])
|
||||
email_list_endpoint = _find_endpoint(email_router, "GET", "/api/email/list")
|
||||
@@ -174,6 +176,7 @@ def setup_codex_routes(
|
||||
documents_library_endpoint = _find_endpoint(document_router, "GET", "/api/documents/library")
|
||||
documents_get_endpoint = _find_endpoint(document_router, "GET", "/api/document/{doc_id}")
|
||||
documents_create_endpoint = _find_endpoint(document_router, "POST", "/api/document")
|
||||
chat_history_endpoint = _find_endpoint(history_router, "GET", "/api/history/{session_id}")
|
||||
|
||||
@router.get("/capabilities")
|
||||
def capabilities(request: Request):
|
||||
@@ -219,6 +222,11 @@ def setup_codex_routes(
|
||||
"launch": scoped(COOKBOOK_LAUNCH_SCOPES),
|
||||
"actions": ["tasks", "servers", "output", "serve", "stop"],
|
||||
},
|
||||
"chat": {
|
||||
"read": scoped(CHAT_READ_SCOPES),
|
||||
"actions": ["read"],
|
||||
"available": chat_history_endpoint is not None,
|
||||
},
|
||||
},
|
||||
"safety": {
|
||||
"email_send_requires_confirmation": True,
|
||||
@@ -226,6 +234,33 @@ def setup_codex_routes(
|
||||
},
|
||||
}
|
||||
|
||||
@router.get("/chat/{session_id}")
|
||||
async def read_chat(
|
||||
request: Request,
|
||||
session_id: str,
|
||||
offset: int = 0,
|
||||
limit: int = 100,
|
||||
):
|
||||
_scope_owner(request, CHAT_READ_SCOPES)
|
||||
if chat_history_endpoint is None:
|
||||
raise HTTPException(503, "chat history unavailable")
|
||||
offset, limit = _clamp_pagination(
|
||||
offset,
|
||||
limit,
|
||||
default_limit=100,
|
||||
max_limit=100,
|
||||
)
|
||||
result = await chat_history_endpoint(request, session_id, limit, offset)
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"name": result["name"],
|
||||
"model": result["model"],
|
||||
"messages": result["history"],
|
||||
"total": result["total"],
|
||||
"offset": result["offset"],
|
||||
"limit": result["limit"],
|
||||
}
|
||||
|
||||
@router.get("/plugin.zip")
|
||||
def plugin_zip(request: Request):
|
||||
require_authenticated_request(request)
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
"""Agent chat history uses the same scope as existing chat and history routes."""
|
||||
|
||||
import pytest
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from starlette.requests import Request
|
||||
|
||||
from routes import codex_routes
|
||||
|
||||
|
||||
def _endpoint(router, method: str, path: str):
|
||||
for route in router.routes:
|
||||
if route.path == path and method in route.methods:
|
||||
return route.endpoint
|
||||
raise AssertionError(f"{method} {path} route not found")
|
||||
|
||||
|
||||
def _request(scopes: list[str]) -> Request:
|
||||
request = Request({
|
||||
"type": "http",
|
||||
"method": "GET",
|
||||
"path": "/api/codex/chat/session-test",
|
||||
"headers": [],
|
||||
"state": {},
|
||||
})
|
||||
request.state.current_user = "api"
|
||||
request.state.api_token = True
|
||||
request.state.api_token_owner = "alice"
|
||||
request.state.api_token_scopes = scopes
|
||||
return request
|
||||
|
||||
|
||||
def _history_router(calls: list[tuple]):
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/api/history/{session_id}")
|
||||
async def history(
|
||||
request: Request,
|
||||
session_id: str,
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
):
|
||||
calls.append((request, session_id, limit, offset))
|
||||
return {
|
||||
"name": "Model debugging",
|
||||
"model": "org/model",
|
||||
"history": [{"role": "assistant", "content": "second"}],
|
||||
"total": 3,
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
"endpoint_url": "http://private-endpoint",
|
||||
}
|
||||
|
||||
return router
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_history_read_uses_existing_paged_handler_without_exposing_endpoint_url():
|
||||
calls = []
|
||||
router = codex_routes.setup_codex_routes(history_router=_history_router(calls))
|
||||
read_chat = _endpoint(router, "GET", "/api/codex/chat/{session_id}")
|
||||
request = _request(["chat"])
|
||||
|
||||
result = await read_chat(request, "session-test", offset=1, limit=1)
|
||||
|
||||
assert calls == [(request, "session-test", 1, 1)]
|
||||
assert result == {
|
||||
"session_id": "session-test",
|
||||
"name": "Model debugging",
|
||||
"model": "org/model",
|
||||
"messages": [{"role": "assistant", "content": "second"}],
|
||||
"total": 3,
|
||||
"offset": 1,
|
||||
"limit": 1,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_token_without_chat_scope_cannot_read_history_or_reach_handler():
|
||||
calls = []
|
||||
router = codex_routes.setup_codex_routes(history_router=_history_router(calls))
|
||||
read_chat = _endpoint(router, "GET", "/api/codex/chat/{session_id}")
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await read_chat(_request(["todos:read"]), "session-test")
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
assert calls == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_history_read_uses_existing_page_limit():
|
||||
calls = []
|
||||
router = codex_routes.setup_codex_routes(history_router=_history_router(calls))
|
||||
read_chat = _endpoint(router, "GET", "/api/codex/chat/{session_id}")
|
||||
|
||||
await read_chat(_request(["chat"]), "session-test", limit=1000)
|
||||
|
||||
assert calls[0][2:] == (100, 0)
|
||||
|
||||
|
||||
def test_capabilities_report_history_permission_from_chat_scope():
|
||||
router = codex_routes.setup_codex_routes(history_router=_history_router([]))
|
||||
capabilities = _endpoint(router, "GET", "/api/codex/capabilities")
|
||||
|
||||
without_chat = capabilities(_request(["todos:read"]))["tools"]["chat"]
|
||||
reader = capabilities(_request(["chat"]))["tools"]["chat"]
|
||||
|
||||
assert without_chat["read"] is False
|
||||
assert reader["read"] is True
|
||||
assert reader["available"] is True
|
||||
assert reader["actions"] == ["read"]
|
||||
Reference in New Issue
Block a user