Skip to content

Commit 8367389

Browse files
internal(assistant): Add assistant backend (#1154)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 892e6af commit 8367389

36 files changed

+3411
-129
lines changed

src/handlers/ai/a2a/assistantAuth.ts

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { app, ipcMain, shell } from 'electron'
1+
import { app, BrowserWindow, ipcMain, shell } from 'electron'
22
import log from 'electron-log/main'
33

44
import { getProfileData } from '@/handlers/auth/fs'
@@ -10,17 +10,19 @@ import {
1010
generateState,
1111
startCallbackServer,
1212
} from '@/services/grafana/assistantAuth'
13+
import { browserWindowFromEvent } from '@/utils/electron'
1314

15+
import { abortAllActiveAssistantSessions } from '../grafanaAssistantProvider'
1416
import { AssistantAuthHandler } from '../types'
1517

18+
import { LOG_PREFIX } from './constants'
1619
import {
1720
clearAssistantTokens,
1821
hasAssistantTokens,
22+
mapTokenResponse,
1923
saveAssistantTokens,
2024
} from './tokenStore'
2125

22-
const PREFIX = '[GrafanaAssistant]'
23-
2426
export type AssistantAuthResult =
2527
| { type: 'authenticated' }
2628
| { type: 'error'; error: string }
@@ -47,11 +49,23 @@ function isAllowedEndpoint(endpoint: string, stackUrl: string): boolean {
4749
}
4850
}
4951

52+
function verificationCode(codeChallenge: string): string {
53+
try {
54+
const hex = Buffer.from(codeChallenge, 'base64url')
55+
.subarray(0, 4)
56+
.toString('hex')
57+
return hex.slice(0, 4) + '-' + hex.slice(4)
58+
} catch {
59+
return '----'
60+
}
61+
}
62+
5063
let pendingAbortController: AbortController | null = null
5164

5265
async function performSignIn(
5366
stackId: string,
54-
stackUrl: string
67+
stackUrl: string,
68+
browserWindow: BrowserWindow
5569
): Promise<AssistantAuthResult> {
5670
const abortController = new AbortController()
5771
pendingAbortController = abortController
@@ -65,22 +79,25 @@ async function performSignIn(
6579
const authUrl = buildAssistantAuthUrl(stackUrl, codeChallenge, state, port)
6680

6781
log.info(
68-
PREFIX,
82+
LOG_PREFIX,
6983
'Initiating assistant auth for stack',
7084
stackId,
7185
'on port',
7286
port
7387
)
7488
void shell.openExternal(authUrl)
7589

90+
const code = verificationCode(codeChallenge)
91+
browserWindow.webContents.send(AssistantAuthHandler.VerificationCode, code)
92+
7693
const callback = await result
7794
app.focus({ steal: true })
7895

7996
if (callback.state !== state) {
80-
log.error(PREFIX, 'State mismatch in assistant auth callback')
97+
log.error(LOG_PREFIX, 'State mismatch in assistant auth callback')
8198
return {
8299
type: 'error',
83-
error: 'State mismatch possible CSRF attack. Please try again.',
100+
error: 'State mismatch, possible CSRF attack. Please try again.',
84101
}
85102
}
86103

@@ -93,7 +110,7 @@ async function performSignIn(
93110

94111
if (!isAllowedEndpoint(callback.endpoint, stackUrl)) {
95112
log.error(
96-
PREFIX,
113+
LOG_PREFIX,
97114
'Callback endpoint does not match expected stack URL:',
98115
callback.endpoint
99116
)
@@ -114,22 +131,18 @@ async function performSignIn(
114131
return { type: 'aborted' }
115132
}
116133

117-
await saveAssistantTokens(stackId, {
118-
accessToken: tokenResponse.token,
119-
refreshToken: tokenResponse.refresh_token,
120-
apiEndpoint: tokenResponse.api_endpoint,
121-
expiresAt: new Date(tokenResponse.expires_at).getTime(),
122-
refreshExpiresAt: new Date(tokenResponse.refresh_expires_at).getTime(),
123-
})
134+
const tokens = mapTokenResponse(tokenResponse, tokenResponse.api_endpoint)
135+
136+
await saveAssistantTokens(stackId, tokens)
124137

125-
log.info(PREFIX, 'Assistant auth completed for stack', stackId)
138+
log.info(LOG_PREFIX, 'Assistant auth completed for stack', stackId)
126139
return { type: 'authenticated' }
127140
} catch (error) {
128141
if (abortController.signal.aborted) {
129142
return { type: 'aborted' }
130143
}
131144

132-
log.error(PREFIX, 'Assistant auth failed:', error)
145+
log.error(LOG_PREFIX, 'Assistant auth failed:', error)
133146
return {
134147
type: 'error',
135148
error: error instanceof Error ? error.message : 'Authentication failed',
@@ -144,7 +157,8 @@ async function performSignIn(
144157
export function initialize() {
145158
ipcMain.handle(
146159
AssistantAuthHandler.SignIn,
147-
async (): Promise<AssistantAuthResult> => {
160+
async (event): Promise<AssistantAuthResult> => {
161+
const browserWindow = browserWindowFromEvent(event)
148162
const profile = await getProfileData()
149163
const stackId = profile.profiles.currentStack
150164

@@ -170,7 +184,7 @@ export function initialize() {
170184
pendingAbortController.abort()
171185
}
172186

173-
return performSignIn(stackId, stack.url)
187+
return performSignIn(stackId, stack.url, browserWindow)
174188
}
175189
)
176190

@@ -207,8 +221,9 @@ export function initialize() {
207221
const stackId = profile.profiles.currentStack
208222

209223
if (stackId) {
224+
abortAllActiveAssistantSessions()
210225
await clearAssistantTokens(stackId)
211-
log.info(PREFIX, 'Cleared assistant tokens for stack', stackId)
226+
log.info(LOG_PREFIX, 'Cleared assistant tokens for stack', stackId)
212227
}
213228
})
214229
}

src/handlers/ai/a2a/cancelTask.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import log from 'electron-log/main'
2+
3+
import type { A2ASessionConfig } from './config'
4+
import { LOG_PREFIX } from './constants'
5+
import { buildA2AHeaders, safeResponseText } from './helpers'
6+
7+
export async function sendTaskCancel(
8+
config: A2ASessionConfig,
9+
taskId: string
10+
): Promise<void> {
11+
const body = {
12+
jsonrpc: '2.0',
13+
id: crypto.randomUUID(),
14+
method: 'tasks/cancel',
15+
params: { id: taskId },
16+
}
17+
18+
const response = await fetch(`${config.baseUrl}/agents/${config.agentId}`, {
19+
method: 'POST',
20+
headers: buildA2AHeaders(config),
21+
body: JSON.stringify(body),
22+
})
23+
24+
if (!response.ok) {
25+
const text = await safeResponseText(response)
26+
log.error(
27+
LOG_PREFIX,
28+
`Failed to cancel task ${taskId} (${response.status}):`,
29+
text
30+
)
31+
}
32+
}

src/handlers/ai/a2a/config.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { getProfileData } from '@/handlers/auth/fs'
2+
3+
import { getValidAssistantTokens } from './tokenRefresh'
4+
5+
export interface A2AConfig {
6+
baseUrl: string
7+
agentId: string
8+
extensions: string
9+
bearerToken: string
10+
}
11+
12+
export type A2ASessionConfig = Omit<A2AConfig, 'extensions'>
13+
14+
const AGENT_ID = 'grafana_assistant_k6_studio'
15+
const REMOTE_TOOL_EXTENSION =
16+
'https://grafana.com/extensions/remote-tool-execution/v1'
17+
const TOKEN_STREAMING_EXTENSION =
18+
'https://grafana.com/extensions/token-streaming/v1'
19+
20+
export async function getA2AConfig(): Promise<A2AConfig> {
21+
const profile = await getProfileData()
22+
const stackId = profile.profiles.currentStack
23+
24+
if (!stackId) {
25+
throw new Error(
26+
'No Grafana Cloud stack selected. Please sign in to Grafana Cloud first.'
27+
)
28+
}
29+
30+
const tokens = await getValidAssistantTokens(stackId)
31+
32+
if (!tokens) {
33+
throw new Error(
34+
'Not authenticated with Grafana Assistant. Please connect to Grafana Assistant first.'
35+
)
36+
}
37+
38+
return {
39+
baseUrl: `${tokens.apiEndpoint}/api/cli/v1/a2a`,
40+
agentId: AGENT_ID,
41+
extensions: [REMOTE_TOOL_EXTENSION, TOKEN_STREAMING_EXTENSION].join(', '),
42+
bearerToken: tokens.accessToken,
43+
}
44+
}

src/handlers/ai/a2a/constants.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export const LOG_PREFIX = '[GrafanaAssistant]'
2+
3+
export const NO_USAGE = {
4+
inputTokens: undefined,
5+
outputTokens: undefined,
6+
totalTokens: undefined,
7+
} as const
8+
9+
/** A2A artifact name constants used in SSE events */
10+
export const ARTIFACT_NAME = {
11+
STEP_TOOL_CALL: 'step.toolCall',
12+
STEP_COMPLETE: 'step.complete',
13+
STEP_MESSAGE: 'step.message',
14+
STEP_TOOL_RESULT: 'step.toolResult',
15+
MESSAGE_STREAM_START: 'message.stream.start',
16+
MESSAGE_CONTENT_DELTA: 'message.content.delta',
17+
MESSAGE_STREAM_COMPLETE: 'message.stream.complete',
18+
} as const

0 commit comments

Comments
 (0)