PKU harness
SDK reference

cloud

The plugin side of PKU harness Cloud. CloudManifest, PluginCloud (store, channels, push, functions), Channel and its events, SecretLink, CloudStatus, CloudError and defineCloudFunction.

PKU harness Cloud is a set of optional cloud services. A plugin can use it to keep a small amount of data on a server, or to share content live through a relay with people who open a share link. This module defines the plugin side of it. A plugin declares the services it needs in its manifest and then calls them through a client that the host provides.

Source: packages/sdk/src/cloud.ts. Guide: Cloud → plugin guide. Wire protocol: @pku-harness/cloud-protocol and the API reference.

What the host keeps for your plugin

A plugin declares the services it uses in PluginDef.cloud and gets a client bound to itself (ctx.cloud, props.cloud). The host keeps the device identity, tokens and keys. It checks that the user has given consent and that each call matches the plugin's manifest, and it talks to the provider the user picked. The provider can be the official server, a self-hosted server or any URL.

Your plugin never sees tokens, keys or provider credentials, and the agent never sees share links.

Host support

The app implements this client. ctx.cloud stays typed as optional (PluginCloud | undefined) for hosts without cloud support, so check ctx.cloud?.available before using it.

push isn't offered yet, and its calls fail with not_configured. The official cloud instance isn't deployed yet. Once it is, it will run only the relay and a small encrypted store, with server functions switched off. Server functions also read their input in plaintext, so the host refuses functions.invoke from a plugin that declares end-to-end encryption (e2e). Keep the work on the phone when you can, and use your own backend when you really need a server.

CloudManifest

CloudManifest is the manifest a plugin declares in PluginDef.cloud. It lists the services the plugin uses, what data leaves the device, and whether that data is end-to-end encrypted.

type CloudService = 'store' | 'channels' | 'push' | 'files' | 'functions';

interface CloudManifest {
  protocol: 1;
  uses: CloudService[];
  /** Plain-language lines shown in the consent sheet: exactly what leaves the device. */
  disclosure: { zh: string[]; en?: string[] };
  /** All store/channel payloads are client-encrypted; the host refuses plaintext writes when true. */
  e2e: boolean;
  /** Plugin still works without cloud; features check ctx.cloud.available. Else requires 'cloud'. */
  optional?: boolean;
  /** Server functions: run on the user's provider (code in plugins/<id>/server) or an external URL. */
  functions?: Record<string, CloudFunctionDecl>;
  backend?: { kind: 'provider' } | { kind: 'external'; url: string };
  /** Static pages from plugins/<id>/web served at /w/<plugin>/<page> (viewers without the app). */
  pages?: string[];
}

interface CloudFunctionDecl<I extends z.ZodType = z.ZodType, O extends z.ZodType = z.ZodType> {
  title: string;
  input: I;
  output: O;
  version: number;
  timeoutMs?: number;
}

function defineCloudFunction<I extends z.ZodType, O extends z.ZodType>(d: CloudFunctionDecl<I, O>): CloudFunctionDecl<I, O>;

PluginCloud

PluginCloud is the client your plugin gets, and CloudStatus describes its current state.

interface PluginCloud {
  /** Provider configured, consent given and reachable (offline-tolerant reads aside). */
  readonly available: boolean;
  status(): CloudStatus;
  /**
   * Ask the user to allow this plugin to use the cloud (the host's consent sheet; resolves at once
   * when already allowed). False when they decline. Screens call it before opening a channel;
   * agent tool calls never prompt and get `CloudError('consent_required')` instead.
   */
  requestConsent(): Promise<boolean>;
  /** Key-value store namespaced to (device, plugin) by the provider. Values are sealed when `e2e`. */
  store: {
    get<T>(key: string): Promise<{ value: T; version: number } | undefined>;
    put<T>(key: string, value: T, opts?: { ifVersion?: number; ttlS?: number }): Promise<{ version: number }>;
    delete(key: string): Promise<void>;
    list(prefix?: string): Promise<{ key: string; version: number; updatedAt: number }[]>;
  };
  /** End-to-end encrypted relay channels (live sharing with people who open a link). */
  channels: {
    /** `meta` is sealed with the link key; viewers read it before joining. */
    open(opts: { ttlS: number; maxPeers?: number; page?: string; meta?: unknown }): Promise<Channel>;
    /** Survives app restarts (keys are kept in secure storage). */
    get(id: string): Channel | undefined;
    list(): Channel[];
  };
  /** Stage 6. */
  push: {
    schedule(at: number, payload: unknown): Promise<string>;
    cancel(id: string): Promise<void>;
  };
  /** Server functions declared in `CloudManifest.functions` (stage 6). */
  functions: {
    invoke<N extends string>(name: N, input: unknown, opts?: { signal?: AbortSignal }): Promise<unknown>;
  };
}

interface CloudStatus {
  provider: 'official' | 'custom' | 'off';
  baseURL?: string;
  /** The user allowed this plugin to use the cloud. */
  consented: boolean;
  online: boolean;
}

Channel

A channel is for live sharing. Your plugin opens a channel and gives someone its share link. Each person who opens the link becomes a peer of the channel.

interface Channel {
  id: string;
  expiresAt: number;
  link: SecretLink;
  peers(): { id: string; joinedAt: number }[];
  /** Subscribe; returns an unsubscribe function. */
  on(ev: ChannelEventType, fn: (e: ChannelEvent) => void): () => void;
  /** Sealed per peer; '*' = every connected peer. */
  send(peerId: string | '*', msg: unknown): Promise<void>;
  /** Sealed blobs readable by peers while the owner is offline (batch mode). */
  mailbox: {
    put(slot: string, msg: unknown, ttlS: number): Promise<void>;
    clear(): Promise<void>;
  };
  /** Drop a peer and free its seat (a new viewer can join with the same link). */
  kick(peerId: string): Promise<void>;
  extend(ttlS: number): Promise<void>;
  close(): Promise<void>;
}

type ChannelEventType = 'join' | 'leave' | 'message' | 'closed';
type ChannelCloseReason = 'owner' | 'expired' | 'revoked' | 'locked' | 'server';

type ChannelEvent =
  | { type: 'join'; peerId: string; at: number }
  | { type: 'leave'; peerId: string; at: number }
  /** A peer's frame, already decrypted (validate it with zod before use). */
  | { type: 'message'; peerId: string; msg: unknown }
  | { type: 'closed'; reason: ChannelCloseReason };
/**
 * An opaque share link (`https://<provider>/s/<id>#k=…`). Plugins can't read the URL: show it with
 * `host.shareLink(link)` or `<QRCode link={link} />`. `JSON.stringify(link)` gives "[secret link]".
 */
interface SecretLink {
  readonly kind: 'secret-link';
  readonly channelId: string;
  toJSON(): string;
}

The host's toString also prints "[secret link]", so a link can't leak into tool results or logs.

Errors

type CloudErrorCode = 'not_configured' | 'consent_required' | 'offline' | 'rate_limited' | 'quota' | 'gone' | 'protocol' | 'server';
const CLOUD_ERROR_CODES: readonly CloudErrorCode[];

class CloudError extends Error {
  readonly code: CloudErrorCode;
  /** Set for 'rate_limited' (from Retry-After). */
  readonly retryAfterS?: number;
  constructor(code: CloudErrorCode, message: string, retryAfterS?: number);
}

function isCloudError(e: unknown, code?: CloudErrorCode): e is CloudError;

The error codes mean the following:

CodeWhen
not_configuredNo provider configured, or the service isn't available (for example push)
consent_requiredThe user hasn't allowed this plugin to use the cloud
offlineNo network. Writes and functions fail right away; there is no silent outbox
rate_limitedThe provider answered 429; retryAfterS is set
quotaStore or channel quota exceeded
goneThe channel was closed or has expired
protocolThe provider's answer doesn't match protocol v1, or the call breaks the manifest (an undeclared service, plaintext from an e2e plugin)
serverAny other provider error

On this page