Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/node/missionControlApiClient.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { IAuthenticationService } from '../../../../platform/authentication/common/authentication';
7
import { INTEGRATION_ID } from '../../../../platform/endpoint/common/licenseAgreement';
8
import { PermissiveAuthRequiredError } from '../../../../platform/github/common/githubService';
9
import { ILogService } from '../../../../platform/log/common/logService';
10
import { IFetcherService } from '../../../../platform/networking/common/fetcherService';
11
12
/** Base path for Mission Control (agent session) endpoints. */
13
const SESSIONS_PATH = '/agents/sessions';
14
15
/** Per-request timeout (ms). */
16
const REQUEST_TIMEOUT_MS = 10_000;
17
18
/** Event payload forwarded to Mission Control. */
19
export interface McEvent {
20
id: string;
21
timestamp: string;
22
parentId: string | null;
23
ephemeral?: boolean;
24
type: string;
25
data: Record<string, unknown>;
26
}
27
28
/** Steering command returned from Mission Control. */
29
export interface McCommand {
30
id: string;
31
content: string;
32
type?: string;
33
state: string;
34
}
35
36
/** Result of a session creation call. */
37
export interface McSessionCreateResult {
38
id: string;
39
taskId: string;
40
}
41
42
/**
43
* Authentication options for Mission Control requests.
44
*
45
* Mission Control endpoints write to the `/agents/sessions` API family, which
46
* requires a permissive GitHub token (same scope as the Copilot Coding Agent
47
* job dispatch endpoint). Callers pass `createIfNone` to surface an interactive
48
* sign-in prompt when no permissive session is available.
49
*/
50
export interface McAuthOptions {
51
/** If provided, shows an interactive permission-upgrade prompt when no silent session exists. */
52
readonly createIfNone?: { readonly detail: string };
53
}
54
55
/**
56
* HTTP client for the Mission Control agent-session API.
57
*
58
* Wraps the four endpoints used by the Copilot CLI `/remote` command:
59
* - `POST /agents/sessions` — create session
60
* - `POST /agents/sessions/{id}/events` — submit events (+ ack completed commands)
61
* - `GET /agents/sessions/{id}/commands` — poll for steering commands
62
* - `DELETE /agents/sessions/{id}` — tear down session
63
*
64
* All requests are routed through {@link IFetcherService} so proxy, custom CA,
65
* and telemetry configuration are applied consistently. Authentication uses a
66
* permissive GitHub session (fetched on each call to pick up token refreshes);
67
* when no permissive session exists and interactive prompting is disabled the
68
* call throws {@link PermissiveAuthRequiredError}, mirroring the pattern used
69
* by `IOctoKitService.postCopilotAgentJob`.
70
*/
71
export class MissionControlApiClient {
72
73
constructor(
74
@IAuthenticationService private readonly _authService: IAuthenticationService,
75
@IFetcherService private readonly _fetcherService: IFetcherService,
76
@ILogService private readonly _logService: ILogService,
77
) { }
78
79
/**
80
* Create a Mission Control session for the given repo and agent task id.
81
*
82
* @throws {PermissiveAuthRequiredError} if `createIfNone` is not set and no silent permissive session exists.
83
*/
84
async createSession(
85
ownerId: number,
86
repoId: number,
87
agentTaskId: string,
88
authOptions: McAuthOptions,
89
): Promise<McSessionCreateResult> {
90
const { url, headers } = await this._buildRequest(SESSIONS_PATH, authOptions);
91
const res = await this._fetcherService.fetch(url, {
92
callSite: 'copilotcli.mc.createSession',
93
method: 'POST',
94
headers,
95
json: {
96
owner_id: ownerId,
97
repo_id: repoId,
98
agent_task_id: agentTaskId,
99
},
100
timeout: REQUEST_TIMEOUT_MS,
101
});
102
if (!res.ok) {
103
const body = await res.text().catch(() => '');
104
throw new Error(`Mission Control session creation failed: ${res.status} ${res.statusText} - ${body}`);
105
}
106
const data = await res.json() as { id: string; task_id?: string };
107
return { id: data.id, taskId: data.task_id ?? agentTaskId };
108
}
109
110
/**
111
* Submit a batch of events to a Mission Control session, optionally
112
* acknowledging completed steering command ids in the same request.
113
*
114
* Returns `true` on success; logs and returns `false` on failure so the
115
* caller can re-queue events.
116
*/
117
async submitEvents(
118
sessionId: string,
119
events: readonly McEvent[],
120
completedCommandIds: readonly string[],
121
): Promise<boolean> {
122
try {
123
const { url, headers } = await this._buildRequest(`${SESSIONS_PATH}/${sessionId}/events`, {});
124
const res = await this._fetcherService.fetch(url, {
125
callSite: 'copilotcli.mc.submitEvents',
126
method: 'POST',
127
headers,
128
json: {
129
events,
130
completed_command_ids: completedCommandIds.length > 0 ? completedCommandIds : undefined,
131
},
132
timeout: REQUEST_TIMEOUT_MS,
133
});
134
if (!res.ok) {
135
const body = await res.text().catch(() => '');
136
this._logService.warn(`[MissionControlApiClient] submitEvents failed: ${res.status} ${res.statusText} - ${body}`);
137
return false;
138
}
139
return true;
140
} catch (err) {
141
this._logService.warn(`[MissionControlApiClient] submitEvents error: ${err}`);
142
return false;
143
}
144
}
145
146
/**
147
* Poll for pending steering commands. Returns an empty array if the
148
* endpoint is unreachable or returns a non-OK response.
149
*/
150
async getPendingCommands(sessionId: string): Promise<McCommand[]> {
151
try {
152
const { url, headers } = await this._buildRequest(`${SESSIONS_PATH}/${sessionId}/commands`, {});
153
const res = await this._fetcherService.fetch(url, {
154
callSite: 'copilotcli.mc.getPendingCommands',
155
method: 'GET',
156
headers,
157
timeout: REQUEST_TIMEOUT_MS,
158
});
159
if (!res.ok) {
160
return [];
161
}
162
const data = await res.json() as { commands?: McCommand[] };
163
return data.commands ?? [];
164
} catch {
165
return [];
166
}
167
}
168
169
/**
170
* Tear down a Mission Control session. Best-effort: failures are swallowed
171
* so `/remote off` always completes locally even if the server is unreachable.
172
*/
173
async deleteSession(sessionId: string): Promise<void> {
174
try {
175
const { url, headers } = await this._buildRequest(`${SESSIONS_PATH}/${sessionId}`, {});
176
await this._fetcherService.fetch(url, {
177
callSite: 'copilotcli.mc.deleteSession',
178
// FetchOptions.method is typed narrowly (GET/POST/PUT) but the
179
// underlying fetcher forwards any string, so DELETE works at runtime.
180
method: 'DELETE' as 'POST',
181
headers,
182
timeout: REQUEST_TIMEOUT_MS,
183
});
184
} catch (err) {
185
this._logService.warn(`[MissionControlApiClient] deleteSession error: ${err}`);
186
}
187
}
188
189
/**
190
* Build the absolute URL and auth headers for an MC request.
191
*
192
* Auth strategy follows `IOctoKitService.postCopilotAgentJob`: request a
193
* permissive GitHub session, optionally with an interactive upgrade prompt.
194
* If no session is available and prompting is disabled, throw
195
* {@link PermissiveAuthRequiredError} so callers can render a dedicated UX.
196
*
197
* The base URL is resolved via the Copilot token's `endpoints.api` which is
198
* GHES-aware.
199
*/
200
private async _buildRequest(
201
path: string,
202
authOptions: McAuthOptions,
203
): Promise<{ url: string; headers: Record<string, string> }> {
204
const session = authOptions.createIfNone
205
? await this._authService.getGitHubSession('permissive', { createIfNone: authOptions.createIfNone })
206
: await this._authService.getGitHubSession('permissive', { silent: true });
207
if (!session?.accessToken) {
208
throw new PermissiveAuthRequiredError();
209
}
210
211
const copilotToken = await this._authService.getCopilotToken();
212
const baseUrl = copilotToken.endpoints?.api;
213
if (!baseUrl) {
214
throw new Error('Copilot API endpoint is not available');
215
}
216
217
const url = `${baseUrl.replace(/\/+$/, '')}${path}`;
218
const headers: Record<string, string> = {
219
'Authorization': `Bearer ${session.accessToken}`,
220
'Copilot-Integration-Id': INTEGRATION_ID,
221
};
222
return { url, headers };
223
}
224
}
225
226