From 7ecc0908efa7d98e759cc03fdb6da453a4894b57 Mon Sep 17 00:00:00 2001 From: Rene Arredondo <120709323+Rene0422@users.noreply.github.com> Date: Sat, 27 Jun 2026 22:00:50 -0700 Subject: [PATCH] fix(agent): authenticate "Thinking" button in shared/embedded chat via beta token (#14985) (#15238) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #14985 — clicking the **Thinking** button in a shared/embedded chat returns 401 and bounces the user to the login page, even though the same share page can chat with the agent just fine. ## Root cause In shared chat, `useGetSharedChatSearchParams` binds `conversationId` to the URL's `shared_id` query param — which is the **beta APIToken**, not the real agent id. That `conversationId` propagates through the component tree: ```tsx → useFetchMessageTrace(canvasId) → GET /api/v1/agents//logs/ ``` But `/agents//logs/` is decorated with `@login_required` (`api/apps/restful_apis/agent_api.py:842-846`). The share page only holds the beta token — there is no session JWT — so the request 401s and quart-auth redirects to the login page. The reporter's server log matches exactly: ``` load_user from jwt got exception No b'.' found in value load_user: No APIToken found for token=ULG10SWG3E... Unauthorized request (quart_auth) GET /api/v1/agents/394013f8d42211f0bad6123fa55e8ed9/logs/96fd72e2-... 1.1 401 ``` The `394013f8...` segment in the URL is the `shared_id` (beta token), not an actual agent id. `_load_user` already accepts the regular `APIToken.token` field, but not `APIToken.beta`, by design — beta is a much weaker share-link credential than a personal API key. The sibling endpoints `/agentbots//completions` and `/agentbots//inputs` already use the right auth pattern for this scope (beta-token via `_get_sdk_authorization_token` → `APIToken.query(beta=token)`). Trace just didn't have a parallel. ## Fix ### Backend (`api/apps/restful_apis/bot_api.py`) Added a beta-token sibling endpoint: ``` GET /api/v1/agentbots//logs/ ``` - Same auth shape as the existing `agentbots` endpoints. - The `` path segment is a client-supplied label only. The real `agent_id` used to build the Redis key (`--logs`) is taken from `APIToken.dialog_id` on the looked-up token, so the endpoint never trusts client-supplied identifiers for the data lookup. - Returns the same `{data: ...}` shape as the existing `/agents//logs/` endpoint, so the frontend doesn't need to reshape the response. ### Frontend - `web/src/utils/api.ts`: added `sharedTrace(sharedId, messageId)` URL builder. - `web/src/services/agent-service.ts`: added `fetchSharedTrace({ shared_id, message_id })`. - `web/src/hooks/use-agent-request.ts`: `useFetchMessageTrace` takes an optional `isShare` argument. When set, it calls `fetchSharedTrace`; `isShare` is also folded into the `queryKey` so the two modes never share cached results. - `web/src/pages/agent/log-sheet/workflow-timeline.tsx`: forwards the already-existing `isShare` prop into the hook. All other existing call sites of `useFetchMessageTrace` (webhook timeline, pipeline log, dataflow result) pass no `isShare` argument → undefined → falsy → unchanged behavior. ## Test plan - [ ] In the regular Agent UI (logged-in user): open the trace / log sheet for any message and click into "Thinking" — the timeline should still load via `/agents//logs/`, same as before. - [ ] From the Agent page, click **Chat in new tab** to open `/chat/share?shared_id=&from=agent`. Send a message, wait for a response, then click **Thinking** on the assistant turn. The trace panel should load instead of redirecting to the login page. - [ ] Same flow but with the agent embedded in an iframe ("Embed into webpage") — confirm there is no login redirect. - [ ] In DevTools → Network, confirm the share-chat trace request goes to `/api/v1/agentbots//logs/` and returns 200 with the same JSON shape as the logged-in path. - [ ] Confirm the chat completions, inputs, and upload flows in the share page still work — they were not touched. - [ ] Send a bogus / expired beta token to the new endpoint and confirm it returns the standard "Authentication error: API key is invalid!" response (no traceback, no 500). - [ ] Run `uv run pytest` to make sure no existing tests regress. ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [ ] New Feature (non-breaking change which adds functionality) - [ ] Documentation Update - [ ] Refactoring - [ ] Performance Improvement - [ ] Other (please describe): --------- Co-authored-by: Zhichang Yu --- api/apps/restful_apis/bot_api.py | 49 +++++++++++++++++++ web/src/hooks/use-agent-request.ts | 22 ++++++--- .../agent/log-sheet/workflow-timeline.tsx | 2 +- web/src/services/agent-service.ts | 9 ++++ web/src/utils/api.ts | 2 + 5 files changed, 77 insertions(+), 7 deletions(-) diff --git a/api/apps/restful_apis/bot_api.py b/api/apps/restful_apis/bot_api.py index 0081157be8..8c0e6e7467 100644 --- a/api/apps/restful_apis/bot_api.py +++ b/api/apps/restful_apis/bot_api.py @@ -14,6 +14,7 @@ # limitations under the License. # import copy +import hashlib import json import re @@ -250,6 +251,54 @@ async def begin_inputs(agent_id, tenant_id=None): "prologue": canvas.get_prologue(), "mode": canvas.get_mode()}) +@manager.route("/agentbots//logs/", methods=["GET"]) # noqa: F821 +async def agent_bot_logs(shared_id, message_id): + # Beta-token sibling of /agents//logs/. + # Used by the shared/embedded chat page's "Thinking" button (fixes #14985). + # The path segment is just the value the client passed in the + # URL (it equals the beta token in the share flow); authentication comes + # from the Authorization header and the real agent_id is read from the + # looked-up APIToken so we never trust client-supplied identifiers. + from rag.utils.redis_conn import REDIS_CONN + + token = _get_sdk_authorization_token() + if not token: + logger.warning( + "agent_bot_logs: missing Authorization header (shared_id=%s message_id=%s)", + shared_id, message_id, + ) + return get_error_data_result(message='Authorization is not valid!') + # Non-reversible fingerprint of the share token: lets operators correlate + # auth-failure log lines for the same token without leaking a guessable + # substring of the secret itself. + token_fp = hashlib.sha256(token.encode("utf-8")).hexdigest()[:16] + objs = await thread_pool_exec(APIToken.query, beta=token) + if not objs: + logger.warning( + "agent_bot_logs: invalid beta token (fingerprint=%s shared_id=%s)", + token_fp, shared_id, + ) + return get_error_data_result(message='Authentication error: API key is invalid!"') + + agent_id = objs[0].dialog_id + if not agent_id: + logger.warning( + "agent_bot_logs: APIToken has no dialog_id (tenant_id=%s fingerprint=%s)", + objs[0].tenant_id, token_fp, + ) + return get_error_data_result(message='API token is not bound to an agent.') + + try: + binary = await thread_pool_exec(REDIS_CONN.get, f"{agent_id}-{message_id}-logs") + if not binary: + return get_json_result(data={}) + payload = binary.decode("utf-8") if isinstance(binary, bytes) else binary + return get_json_result(data=json.loads(payload)) + except Exception as exc: + logging.exception(exc) + return server_error_response(exc) + + @manager.route("/searchbots/ask", methods=["POST"]) # noqa: F821 @login_required(auth_types=AUTH_BETA) @add_tenant_id_to_kwargs diff --git a/web/src/hooks/use-agent-request.ts b/web/src/hooks/use-agent-request.ts index 715613f17e..f0728a4630 100644 --- a/web/src/hooks/use-agent-request.ts +++ b/web/src/hooks/use-agent-request.ts @@ -26,6 +26,7 @@ import agentService, { fetchAgentLogsByCanvasId, fetchAgentLogsById, fetchPipeLineList, + fetchSharedTrace, fetchTrace, fetchWebhookTrace, updateAgent, @@ -529,8 +530,12 @@ export const useUploadAgentFileWithProgress = (identifier?: string | null) => { return { data, loading, uploadAgentFile: mutateAsync }; }; -export const useFetchMessageTrace = (canvasId?: string) => { +export const useFetchMessageTrace = (canvasId?: string, isShare?: boolean) => { const { id } = useParams(); + // In shared mode there's no :id route param and `canvasId` actually carries + // the share (beta) APIToken — route through fetchSharedTrace so the request + // hits the beta-token-aware endpoint instead of /agents//logs which + // requires a session login (fixes #14985). const queryId = id || canvasId; const [messageId, setMessageId] = useState(''); const [isStopFetchTrace, setISStopFetchTrace] = useState(false); @@ -540,7 +545,7 @@ export const useFetchMessageTrace = (canvasId?: string) => { isFetching: loading, refetch, } = useQuery({ - queryKey: [AgentApiAction.Trace, queryId, messageId], + queryKey: [AgentApiAction.Trace, queryId, messageId, !!isShare], refetchOnReconnect: false, refetchOnMount: false, refetchOnWindowFocus: false, @@ -548,10 +553,15 @@ export const useFetchMessageTrace = (canvasId?: string) => { enabled: !!queryId && !!messageId, refetchInterval: !isStopFetchTrace ? 3000 : false, queryFn: async () => { - const { data } = await fetchTrace({ - canvas_id: queryId as string, - message_id: messageId, - }); + const { data } = isShare + ? await fetchSharedTrace({ + shared_id: queryId as string, + message_id: messageId, + }) + : await fetchTrace({ + canvas_id: queryId as string, + message_id: messageId, + }); return Array.isArray(data?.data) ? data?.data : []; }, diff --git a/web/src/pages/agent/log-sheet/workflow-timeline.tsx b/web/src/pages/agent/log-sheet/workflow-timeline.tsx index 4b8b3eed15..5a78f90b6a 100644 --- a/web/src/pages/agent/log-sheet/workflow-timeline.tsx +++ b/web/src/pages/agent/log-sheet/workflow-timeline.tsx @@ -103,7 +103,7 @@ export const WorkFlowTimeline = ({ data: traceData, setMessageId, setISStopFetchTrace, - } = useFetchMessageTrace(canvasId); + } = useFetchMessageTrace(canvasId, isShare); useEffect(() => { setMessageId(currentMessageId); diff --git a/web/src/services/agent-service.ts b/web/src/services/agent-service.ts index 92dcfeaf65..fee6709eae 100644 --- a/web/src/services/agent-service.ts +++ b/web/src/services/agent-service.ts @@ -154,6 +154,15 @@ export const fetchTrace = (data: { canvas_id: string; message_id: string }) => { }), ); }; + +// Used by the shared/embedded chat page where the only credential available +// is the share (beta) APIToken (fixes #14985). +export const fetchSharedTrace = (data: { + shared_id: string; + message_id: string; +}) => { + return request.get(api.sharedTrace(data.shared_id, data.message_id)); +}; export const fetchAgentLogsByCanvasId = ( canvasId: string, params: IAgentLogsRequest, diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index 690baf252f..806090d277 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -256,6 +256,8 @@ export default { `${restAPIv1}/agents/${agentId}/components/${componentId}/debug`, trace: (agentId: string, messageId: string) => `${restAPIv1}/agents/${agentId}/logs/${messageId}`, + sharedTrace: (sharedId: string, messageId: string) => + `${restAPIv1}/agentbots/${sharedId}/logs/${messageId}`, cancelCanvas: (taskId: string) => `${restAPIv1}/tasks/${taskId}/cancel`, // agent inputForm: (agentId: string, componentId: string) =>