Skip to content

Commit 51d6d74

Browse files
feat(quantum): add native quantum computing tools replacing pod_mcp
- Add TypeScript HTTP client for qBraid quantum API with auth resolution chain (env var > CodeQ auth store > ~/.qbraid/qbraidrc) - Define 6 in-process tools: quantum_devices, quantum_estimate_cost, quantum_submit_job, quantum_get_result, quantum_cancel_job, quantum_list_jobs - Job submission uses ctx.ask() permission system for cost approval, replacing pod_mcp's fragile cost_reviewed_and_approved boolean - Register quantum tools in tool registry - Tightly integrated with CodeQ auth, permissions, and telemetry; not portable to other agents by design
1 parent 780b18c commit 51d6d74

4 files changed

Lines changed: 556 additions & 0 deletions

File tree

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
/**
2+
* qBraid Quantum API Client
3+
*
4+
* TypeScript HTTP client for the qBraid quantum runtime API.
5+
* Replaces the Python SDK dependency for device listing, job submission,
6+
* and result retrieval — keeping everything in-process.
7+
*/
8+
9+
import { Log } from "../util/log"
10+
import { Auth } from "../auth"
11+
import path from "path"
12+
import os from "os"
13+
import fs from "fs/promises"
14+
15+
const log = Log.create({ service: "quantum:client" })
16+
17+
const DEFAULT_API_URL = "https://api-v2.qbraid.com/api/v1"
18+
19+
export interface QuantumDevice {
20+
id: string
21+
name: string
22+
vendor: string
23+
provider: string
24+
type: string
25+
status: string
26+
qubits: number
27+
paradigm: string
28+
pricing?: {
29+
perShot?: number
30+
perTask?: number
31+
perMinute?: number
32+
}
33+
}
34+
35+
export interface QuantumJob {
36+
id: string
37+
device: string
38+
status: string
39+
shots: number
40+
createdAt: string
41+
endedAt?: string
42+
cost?: number
43+
}
44+
45+
export interface JobResult {
46+
jobId: string
47+
status: string
48+
measurements?: Record<string, number>
49+
success: boolean
50+
}
51+
52+
export interface CostEstimate {
53+
deviceId: string
54+
shots: number
55+
estimatedCredits: number
56+
breakdown: {
57+
perShot: number
58+
perTask: number
59+
}
60+
}
61+
62+
/**
63+
* Resolve the qBraid API key and base URL.
64+
* Priority: env var > config provider > ~/.qbraid/qbraidrc
65+
*/
66+
async function resolveAuth(): Promise<{ apiKey: string; baseUrl: string } | null> {
67+
let apiKey: string | undefined
68+
let baseUrl = DEFAULT_API_URL
69+
70+
// 1. Environment variable
71+
if (process.env.QBRAID_API_KEY) {
72+
apiKey = process.env.QBRAID_API_KEY
73+
}
74+
75+
// 2. CodeQ auth store
76+
if (!apiKey) {
77+
try {
78+
const authData = await Auth.all()
79+
for (const [key, value] of Object.entries(authData)) {
80+
if (key.includes("qbraid")) {
81+
if (value.type === "wellknown" && value.token) {
82+
apiKey = value.token
83+
break
84+
}
85+
if (value.type === "api" && value.key) {
86+
apiKey = value.key
87+
break
88+
}
89+
}
90+
}
91+
} catch {
92+
// auth not available
93+
}
94+
}
95+
96+
// 3. ~/.qbraid/qbraidrc
97+
if (!apiKey) {
98+
try {
99+
const rcPath = path.join(os.homedir(), ".qbraid", "qbraidrc")
100+
const content = await fs.readFile(rcPath, "utf-8")
101+
for (const line of content.split("\n")) {
102+
const keyMatch = line.trim().match(/^api-key\s*=\s*(.+)/)
103+
if (keyMatch) apiKey = keyMatch[1].trim()
104+
const urlMatch = line.trim().match(/^url\s*=\s*(.+)/)
105+
if (urlMatch) baseUrl = urlMatch[1].trim()
106+
}
107+
} catch {
108+
// qbraidrc not available
109+
}
110+
}
111+
112+
if (process.env.QBRAID_API_BASE_URL) {
113+
baseUrl = process.env.QBRAID_API_BASE_URL
114+
}
115+
116+
if (!apiKey) return null
117+
return { apiKey, baseUrl }
118+
}
119+
120+
async function request<T>(method: string, endpoint: string, body?: unknown): Promise<T> {
121+
const auth = await resolveAuth()
122+
if (!auth) throw new Error("No qBraid API key found. Run `codeq /connect` to set up qBraid.")
123+
124+
const url = `${auth.baseUrl}${endpoint}`
125+
log.debug("quantum api request", { method, url })
126+
127+
const response = await fetch(url, {
128+
method,
129+
headers: {
130+
"Content-Type": "application/json",
131+
"api-key": auth.apiKey,
132+
},
133+
body: body ? JSON.stringify(body) : undefined,
134+
})
135+
136+
if (!response.ok) {
137+
const text = await response.text().catch(() => "")
138+
throw new Error(`qBraid API ${method} ${endpoint} failed (${response.status}): ${text}`)
139+
}
140+
141+
return response.json() as Promise<T>
142+
}
143+
144+
/**
145+
* List available quantum devices with optional filters.
146+
*/
147+
export async function listDevices(filters?: {
148+
status?: string
149+
provider?: string
150+
}): Promise<QuantumDevice[]> {
151+
const params = new URLSearchParams()
152+
if (filters?.status) params.set("status", filters.status)
153+
if (filters?.provider) params.set("provider", filters.provider)
154+
155+
const query = params.toString()
156+
const endpoint = `/quantum/devices${query ? `?${query}` : ""}`
157+
const data = await request<QuantumDevice[] | { devices: QuantumDevice[] }>("GET", endpoint)
158+
return Array.isArray(data) ? data : data.devices ?? []
159+
}
160+
161+
/**
162+
* Get details for a specific device.
163+
*/
164+
export async function getDevice(deviceId: string): Promise<QuantumDevice> {
165+
return request<QuantumDevice>("GET", `/quantum/devices/${encodeURIComponent(deviceId)}`)
166+
}
167+
168+
/**
169+
* Estimate the cost of running a job on a device.
170+
*/
171+
export async function estimateCost(deviceId: string, shots: number): Promise<CostEstimate> {
172+
const device = await getDevice(deviceId)
173+
const perShot = device.pricing?.perShot ?? 0
174+
const perTask = device.pricing?.perTask ?? 0
175+
const estimatedCredits = perShot * shots + perTask
176+
177+
return {
178+
deviceId,
179+
shots,
180+
estimatedCredits,
181+
breakdown: { perShot: perShot * shots, perTask },
182+
}
183+
}
184+
185+
/**
186+
* Submit a QASM circuit to a device.
187+
*/
188+
export async function submitJob(params: {
189+
deviceId: string
190+
qasm: string
191+
shots: number
192+
}): Promise<QuantumJob> {
193+
return request<QuantumJob>("POST", "/quantum/jobs", {
194+
device: params.deviceId,
195+
openQasm: params.qasm,
196+
shots: params.shots,
197+
})
198+
}
199+
200+
/**
201+
* Get the status and metadata of a job.
202+
*/
203+
export async function getJob(jobId: string): Promise<QuantumJob> {
204+
return request<QuantumJob>("GET", `/quantum/jobs/${encodeURIComponent(jobId)}`)
205+
}
206+
207+
/**
208+
* Get the results of a completed job.
209+
*/
210+
export async function getResult(jobId: string): Promise<JobResult> {
211+
return request<JobResult>("GET", `/quantum/jobs/${encodeURIComponent(jobId)}/result`)
212+
}
213+
214+
/**
215+
* Cancel a running or queued job.
216+
*/
217+
export async function cancelJob(jobId: string): Promise<{ success: boolean }> {
218+
return request<{ success: boolean }>("POST", `/quantum/jobs/${encodeURIComponent(jobId)}/cancel`)
219+
}
220+
221+
/**
222+
* List recent jobs with optional filters.
223+
*/
224+
export async function listJobs(filters?: {
225+
status?: string
226+
limit?: number
227+
}): Promise<QuantumJob[]> {
228+
const params = new URLSearchParams()
229+
if (filters?.status) params.set("status", filters.status)
230+
if (filters?.limit) params.set("limit", String(filters.limit))
231+
232+
const query = params.toString()
233+
const endpoint = `/quantum/jobs${query ? `?${query}` : ""}`
234+
const data = await request<QuantumJob[] | { jobs: QuantumJob[] }>("GET", endpoint)
235+
return Array.isArray(data) ? data : data.jobs ?? []
236+
}
237+
238+
/**
239+
* Get account credit balance.
240+
*/
241+
export async function getCredits(): Promise<{ balance: number }> {
242+
return request<{ balance: number }>("GET", "/user/credits")
243+
}
244+
245+
/**
246+
* Check if qBraid API access is configured.
247+
*/
248+
export async function isConfigured(): Promise<boolean> {
249+
const auth = await resolveAuth()
250+
return auth !== null
251+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Quantum Module
3+
*
4+
* Native quantum computing integration for CodeQ.
5+
* Provides in-process tools for device management, job submission,
6+
* cost estimation, and result retrieval via the qBraid API.
7+
*
8+
* This replaces the pod_mcp MCP server for core quantum workflows,
9+
* giving CodeQ tight integration with auth, permissions, and telemetry.
10+
*/
11+
12+
export { QUANTUM_TOOLS } from "./tools"
13+
export * as QuantumClient from "./client"

0 commit comments

Comments
 (0)