Hardware providers
A hardware provider connects the kernel to one vendor’s queue. Implement
the ABC in kernel/hardware/base.py, register one instance in the
manager, add a credential form entry in the frontend — the job tracker,
persistence, and polling come for free.
The HardwareProvider ABC
Section titled “The HardwareProvider ABC”| Method | Contract |
|---|---|
connect(credentials) -> bool | Authenticate with the vendor SDK. Return False on bad credentials or missing SDK — don’t raise. Print a hint (it reaches the kernel log). |
list_backends() -> list[BackendInfo] | Return BackendInfo entries; [] when not connected. Swallow per-backend SDK errors and skip the entry. |
submit_job(circuit_obj, backend, shots) -> JobHandle | Submit and return a JobHandle. See the failure convention below. |
get_results(job) -> dict | {"measurements": {...}, "status": "complete"} when done; {"status": "running", ...} while pending; {"error": ...} on failure or unknown id — dicts, not exceptions. |
get_queue_position(job) -> int | Current position; -1 when unknown. |
cancel_job(job) -> bool | Optional override. The inherited default no-ops and returns True — correct for synchronous providers (local simulator, NVIDIA). |
The failure convention: return a failed handle, don’t raise
Section titled “The failure convention: return a failed handle, don’t raise”The Azure provider is the model:
- Raise (
RuntimeError) only for misuse —submit_jobcalled beforeconnect. The manager raises the same way for unconnected providers , and the server turns the exception into the bare hardware error envelope. - Every provider-side failure returns a
JobHandlewithstatus="failed"and a human-readableerror— target lookup failed, target not found, SDK submit exploded. The frontend then receives a structuredhardware_job_submittedcarrying the failed job instead of a string, and the job appears in the tracker with its error. - Distinguish failure messages by actionability: “target not found — check the name” (user can act) vs “submit failed: <SDK error>” (file a bug / retry).
Registering the provider
Section titled “Registering the provider”One line in HardwareManager.__init__ — the dict key is the provider id
used in every hardware_* message:
self._providers = { "simulator": SimulatorProvider(), ... "echo": EchoProvider(), # your provider}The constructor must be cheap and must not import the vendor SDK —
providers are instantiated unconditionally at kernel start; import the SDK
lazily inside connect() like every existing provider does.
What the manager does for you (JobStore interplay)
Section titled “What the manager does for you (JobStore interplay)”You keep SDK job objects in memory; the manager owns everything durable:
- On submit: registers the handle and immediately persists a
JobRecord(id, provider, backend, status, shots, timestamps) to the file-backed JobStore, so a kernel crash between submit and first poll doesn’t orphan the job. - On polling: write-through of queue-position updates for
queued/runningjobs. - On restart: terminal jobs (
complete/failed/cancelled) reload verbatim; non-terminal jobs come back asstale(your SDK handle is gone) and are never polled against your provider. You don’t handle rehydration at all. - On connect: credentials persist to the OS keyring and auto-reconnect on the next kernel start — silently, with failures logged not raised.
Credential wiring (and the key-name gotcha)
Section titled “Credential wiring (and the key-name gotcha)”The kernel-side field names are the contract. Whatever keys your
connect(credentials) reads must be exactly the keys the frontend sends.
- Define the keys your provider reads, e.g.
credentials.get("token", ""). - Add an entry to
PROVIDER_CONFIGinsrc/components/hardware/CredentialSetup.tsx:13— label, description, andfields: [{ key, label, placeholder }]. Thekeyof each field becomes the credentials-dict key sent to the kernel, and the dialog sends credentials straight to the kernel (never to localStorage). - Add the provider id to the
HardwareProviderTypeunion insrc/types/hardware.ts.
Worked example: EchoProvider
Section titled “Worked example: EchoProvider”The smallest possible provider, modeled on
kernel/hardware/simulator_provider.py: connect always succeeds, one fake
backend, and submit_job completes synchronously, echoing the requested
shot count into a fake counts dict. The file lives at
docs-site/fixtures/examples/echo_provider.py and
kernel/tests/test_docs_examples.py injects it into a real
HardwareManager (the same injection pattern as
kernel/tests/hardware/test_manager.py), connects, submits, and asserts
the results round-trip — so this example cannot rot.
"""Worked example for /docs/extending/hardware-providers/ — the smallestpossible HardwareProvider.
Modeled on kernel/hardware/simulator_provider.py: jobs completesynchronously, and "results" just echo the requested shot count into a fakecounts dict. kernel/tests/test_docs_examples.py runs this file against areal HardwareManager so the example cannot rot."""
import uuidfrom datetime import datetime, timezone
from kernel.hardware.base import BackendInfo, HardwareProvider, JobHandle
ECHO_BACKEND = BackendInfo( name="echo_1", provider="echo", qubit_count=8, connectivity=[(i, i + 1) for i in range(7)], queue_length=0, average_error_rate=0.0, gate_set=["H", "X", "CNOT", "Measure"], status="online",)
class EchoProvider(HardwareProvider): def __init__(self): self._results: dict[str, dict] = {}
def connect(self, credentials: dict) -> bool: # Validate credentials and authenticate with the vendor SDK here. # Return False (don't raise) when auth fails. Echo accepts anything. return True
def list_backends(self) -> list[BackendInfo]: # Return [] when not connected; never raise from here. return [ECHO_BACKEND]
def submit_job(self, circuit_obj, backend: str, shots: int) -> JobHandle: job_id = str(uuid.uuid4()) now = datetime.now(timezone.utc).isoformat() # Synchronous "hardware": the job is complete before submit returns. # A real provider returns status="queued" here and stashes the # vendor SDK's job object so get_results / get_queue_position can # poll it later. Provider-side failures must be RETURNED as a # status="failed" handle with `error` set — never raised. self._results[job_id] = { "measurements": {"00": shots}, # echo the shot count back "status": "complete", } return JobHandle( id=job_id, provider="echo", backend=backend, status="complete", queue_position=None, shots=shots, submitted_at=now, )
def get_results(self, job: JobHandle) -> dict: # Unknown ids and provider failures come back as {"error": ...} # dicts, not exceptions. return self._results.get(job.id, {"error": f"Job {job.id} not found"})
def get_queue_position(self, job: JobHandle) -> int: return 0
# cancel_job: the inherited default (no-op returning True) is correct # for synchronous providers. Queue-backed providers override it.What the test verifies:
connect_provider("echo", {})→True;list_backends("echo")returns the one backend (and[]before connecting — manager-enforced).submit_jobreturns acompletehandle; the manager registered and persisted it (get_job_statusworks immediately).get_resultsreturns{"measurements": {"00": shots}, "status": "complete"}.- Submitting while unconnected raises
RuntimeError— from the manager, per the convention above.
Testing expectations
Section titled “Testing expectations”Provider tests live in kernel/tests/hardware/, whose conftest.py
isolates the OS keyring and job store automatically. Mock the vendor SDK
at the import boundary (see test_ionq_provider.py,
test_azure_provider.py) — CI installs no vendor SDKs. Cover at minimum:
connect success/failure, list_backends when disconnected, the
failed-handle submit path, and the results dict shapes for
complete/running/failed/unknown.