Skip to content

Custom services

A connection adapter can expose operations that are not files, a console or settings — reading a sensor, restarting a service, querying a device API. Apps reach them through client.services, with a permission per service.

Availability

Declare the service you need in your manifest as services.<service-id>, for example:

json
{ "permissions": ["services.acme.sensor"] }

That grants every method the acme.sensor service advertises. The service must be present on the current workspace connection: without it, calls fail with unavailable rather than silently doing nothing.

Service identifiers are lowercase, dot-separated, and may not start with system, files, console, host, terminal or services — those namespaces belong to the desktop.

Interface

ts
client.services.list(signal?: AbortSignal): Promise<readonly ServiceMethodInfo[]>;
client.services.call(method: string, params?: Json, signal?: AbortSignal): Promise<Json>;

Custom entries appear in list() alongside built-in methods, with the service's permissions, whether they are granted, whether they are available, and a source identifying which connection provides them. Method names are always <service-id>.<method>.

The adapter defines the parameters and the result. Both are plain JSON, and validating the result is your app's job — the desktop checks identity and permissions, not meaning.

Examples

Discover before calling:

ts
const methods = await client.services.list();
const sensor = methods.find((m) => m.name === "acme.sensor.read");
if (!sensor?.granted) return hide();            // not permitted for this app
if (!sensor.available) return showOffline();    // adapter missing or disconnected

Call it, with a timeout of your own:

ts
import { RpcError } from "@techartdev/shellcanvas-app-sdk";

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 10_000);
try {
  const result = await client.services.call("acme.sensor.read", { probe: "inlet" }, controller.signal);
  const value = (result as { celsius?: number }).celsius;
  if (typeof value !== "number") throw new Error("Unexpected adapter response.");
  render(value);
} catch (error) {
  if (error instanceof RpcError && error.code === "unavailable") showOffline();
  else throw error;
} finally {
  clearTimeout(timer);
}

Re-run discovery when the environment changes: switching or replacing a connection can add or remove services.

ts
client.events.subscribe((batch) => {
  if (batch.events.some((e) => e.topic === "system.services")) void refreshServiceButtons();
});

Errors

CodeMessageMeaning
invalidProvide a custom service method and JSON parametersMalformed call.
deniedThis app needs the services.<service> permissionNot declared, or not approved.
unavailableThis workspace does not expose that service methodNo adapter provides it here.
unavailableThe selected service is unavailablePresent but not usable right now.
unavailableThe accepted workspace connection is unavailableThe workspace is disconnected.
abortedService call canceledYour signal fired.
closedService connection changed; the remote outcome may be uncertain. Inspect before retrying.A replacement happened mid-call.

Anything the adapter itself reports arrives with the adapter's own code and message.

Lifecycle and permissions

While a workspace is disconnected, list() still returns the services it knows about with available: false, so your interface keeps its shape instead of emptying and refilling.

A granted service permission covers every method that service advertises — there is no per-method approval — so scope matters when you ask for it. It grants nothing else: no files, no console, no network.

Adapters are native programs the user installed and trusted. Their operations run with the user's operating-system permissions, and an uncertain outcome means exactly that: inspect the device before retrying an action that may have taken effect.

Today no production adapters ship with ShellCanvas: SSH provides the built-in services, and custom services come from adapters a user installs. Build against discovery, so your app degrades cleanly where a device is absent.

ShellCanvas documentation