Path: blob/main/extensions/copilot/src/extension/chronicle/node/cloudSessionApiClient.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { IAuthenticationService } from '../../../platform/authentication/common/authentication';6import { ICopilotTokenManager } from '../../../platform/authentication/common/copilotTokenManager';7import { INTEGRATION_ID } from '../../../platform/endpoint/common/licenseAgreement';8import { IFetcherService } from '../../../platform/networking/common/fetcherService';9import type { CreateSessionFailureReason, CreateSessionResult, CloudSession, SessionEvent } from '../common/cloudSessionTypes';1011/** Timeout for individual cloud API requests (ms). */12const REQUEST_TIMEOUT_MS = 10_000;1314/** Cloud sessions endpoint path. */15const SESSIONS_PATH = '/agents/sessions';1617/**18* HTTP client for the cloud session API.19*20* Creates sessions and submits event batches. All methods are non-blocking:21* failures are logged but never thrown to avoid disrupting the chat session.22*/23export class CloudSessionApiClient {2425constructor(26private readonly _tokenManager: ICopilotTokenManager,27private readonly _authService: IAuthenticationService,28private readonly _fetcherService: IFetcherService,29) { }3031/**32* Create a session in the cloud.33*34* The response includes both the session ID and the associated task ID.35*/36async createSession(37ownerId: number,38repoId: number,39sessionId: string,40indexingLevel: 'user' | 'repo_and_user' = 'user',41): Promise<CreateSessionResult> {42try {43const { url, headers } = await this._buildRequest(SESSIONS_PATH);44if (!url) {45return { ok: false, reason: 'error' };46}4748const body = {49owner_id: ownerId,50repo_id: repoId,51agent_task_id: sessionId,52indexing_level: indexingLevel,53};5455const res = await this._fetcherService.fetch(url, {56callSite: 'chronicle.cloudCreateSession',57method: 'POST',58headers,59json: body,60timeout: REQUEST_TIMEOUT_MS,61});6263if (!res.ok) {64const reason: CreateSessionFailureReason = res.status === 403 ? 'policy_blocked' : 'error';65return { ok: false, reason };66}6768const response = await res.json() as { id: string; task_id?: string; agent_task_id?: string };69return { ok: true, response };70} catch (err) {71return { ok: false, reason: 'error' };72}73}7475/**76* Submit a batch of events to a session.77* @returns true if the submission succeeded.78*/79async submitSessionEvents(80sessionId: string,81events: SessionEvent[],82): Promise<boolean> {83try {84const { url, headers } = await this._buildRequest(`${SESSIONS_PATH}/${sessionId}/events`);85if (!url) {86return false;87}8889const res = await this._fetcherService.fetch(url, {90callSite: 'chronicle.cloudSubmitEvents',91method: 'POST',92headers,93json: { events },94timeout: REQUEST_TIMEOUT_MS,95});9697if (!res.ok) {98return false;99}100101return true;102} catch (err) {103return false;104}105}106107/**108* Get a session by ID (used for reattach verification).109*/110async getSession(sessionId: string): Promise<CloudSession | undefined> {111try {112const { url, headers } = await this._buildRequest(`${SESSIONS_PATH}/${sessionId}`);113if (!url) {114return undefined;115}116117const res = await this._fetcherService.fetch(url, {118callSite: 'chronicle.cloudGetSession',119method: 'GET',120headers,121timeout: REQUEST_TIMEOUT_MS,122});123124if (!res.ok) {125return undefined;126}127128return (await res.json()) as CloudSession;129} catch {130return undefined;131}132}133134/**135* Build the full URL and auth headers for a cloud API request.136*/137private async _buildRequest(path: string): Promise<{ url: string | undefined; headers: Record<string, string> }> {138try {139const copilotToken = await this._tokenManager.getCopilotToken();140const baseUrl = copilotToken.endpoints?.api;141if (!baseUrl) {142return { url: undefined, headers: {} };143}144145// Prefer GitHub OAuth token, fallback to Copilot token146const githubToken = this._authService.anyGitHubSession?.accessToken;147const bearerToken = githubToken ?? copilotToken.token;148149const url = `${baseUrl.replace(/\/+$/, '')}${path}`;150const headers: Record<string, string> = {151'Content-Type': 'application/json',152'Authorization': `Bearer ${bearerToken}`,153'Copilot-Integration-Id': INTEGRATION_ID,154};155156return { url, headers };157} catch {158return { url: undefined, headers: {} };159}160}161}162163164