BlaiseBlaise
Documentation
Runner and PlugsReference

Host API types

The type declarations of @blaiseagent/runner/host, with their TSDoc.

The building blocks for plugs that provide a host, from @blaiseagent/runner/host. The prose reference is Host API.

bindHost

/**
 * The host as one plug sees it as `ctx.host`: `exec` starts in `agent.cwd` with `agent.env`
 * layered under the call's own `env`, and relative `fs` paths resolve against `agent.cwd`. The
 * agent draft is read at call time, so a change made by an outer plug is seen at once.
 */
export function bindHost(host: Host, agent: Pick<AgentSpawnSpec, "cwd" | "env">): Host;

ByteStream

/** A two-way byte stream: a WebSocket, a TCP socket, a child's stdio, … */
export interface ByteStream {
  readonly readable: ReadableStream<Uint8Array>;
  readonly writable: WritableStream<Uint8Array>;
}

closeHost

/**
 * Releases the connection behind a host made by `createHost`. Hosts made any other way (the
 * local host, `wrapHost`) own nothing, and closing them does nothing.
 */
export function closeHost(host: Host): Promise<void>;

createHost

/**
 * Builds a lazy `Host`: `connect` runs on first use (an `exec`, an `fs` call, the agent's
 * spawn) and at most once; concurrent first uses share the call. `exec` and `spawn` run over the
 * connection's transport, and `fs` is the vfs API over its provider (an `ExecProvider` unless the
 * connection brings its own). The runner closes the connection when the `next()` that
 * introduced the host resolves.
 */
export function createHost(options: CreateHostOptions): Host;

CreateHostOptions

export interface CreateHostOptions {
  readonly info: HostInfo;
  /**
   * Creates or wakes the environment and connects to it. Runs at most once, on first use;
   * concurrent first uses share the same call.
   */
  readonly connect: () => Promise<HostConnection>;
}

ExecProvider

/**
 * A file system provider for any host that can run POSIX tools: every operation is a small
 * command over `transport.spawn` (`stat`, `cat`, `find`, `mkdir`, `mv`, …). It supports the async
 * API only; the sync methods throw `ENOTSUP`.
 *
 * It is the default for hosts whose connection offers nothing better (SSH offers SFTP).
 */
export class ExecProvider extends VirtualProvider {
  #private;
  private readonly transport;
  constructor(transport: ProcessTransport);
  get supportsSymlinks(): boolean;
  private run;
  private flavour;
  private statOf;
  stat(path: string): Promise<VirtualStats>;
  lstat(path: string): Promise<VirtualStats>;
  readdir(path: string, options?: ReaddirOptions): Promise<string[] | VirtualDirent[]>;
  mkdir(path: string, options?: MkdirOptions): Promise<string | undefined>;
  rmdir(path: string): Promise<void>;
  unlink(path: string): Promise<void>;
  rename(oldPath: string, newPath: string): Promise<void>;
  readFile(path: string, options?: Encoding): Promise<Buffer | string>;
  writeFile(
    path: string,
    data: string | Buffer,
    options?:
      | {
          encoding?: BufferEncoding;
          mode?: number;
        }
      | BufferEncoding
  ): Promise<void>;
  appendFile(
    path: string,
    data: string | Buffer,
    options?:
      | {
          encoding?: BufferEncoding;
          mode?: number;
        }
      | BufferEncoding
  ): Promise<void>;
  private write;
  exists(path: string): Promise<boolean>;
  copyFile(src: string, dest: string): Promise<void>;
  realpath(path: string): Promise<string>;
  access(path: string, mode?: number): Promise<void>;
  readlink(path: string): Promise<string>;
  symlink(target: string, path: string): Promise<void>;
  openSync(path: string): never;
  statSync(path: string): never;
  lstatSync(path: string): never;
  readdirSync(path: string): never;
  mkdirSync(path: string): never;
  rmdirSync(path: string): never;
  unlinkSync(path: string): never;
  renameSync(oldPath: string): never;
  readFileSync(path: string): never;
  writeFileSync(path: string): never;
  appendFileSync(path: string): never;
  existsSync(): never;
  copyFileSync(src: string): never;
  realpathSync(path: string): never;
  accessSync(path: string): never;
  readlinkSync(path: string): never;
  symlinkSync(_target: string, path: string): never;
}

generateSessionKey

/**
 * An ephemeral ed25519 keypair in OpenSSH formats, for one session's access to a machine.
 *
 * `ssh2` on Bun occasionally (about one in a hundred) generates a pair that its own parser then
 * rejects, so each pair is checked and a bad one is replaced.
 */
export function generateSessionKey(): {
  publicKey: string;
  privateKey: string;
};

HostConnection

/** What `connect` returns: the live connection behind a lazy host. */
export interface HostConnection {
  readonly transport: ProcessTransport;
  /**
   * Async fs provider. The default is an exec-based provider over `transport`
   * (`find`, `cat`, `mkdir`, `cp` and other coreutils over `spawn`). SSH hosts pass SFTP.
   */
  readonly fs?: VirtualProvider;
  readonly forward?: (spec: ForwardSpec) => Promise<AsyncDisposable>;
  /** Enables the sync `fs` methods via a worker; omitted means they throw ENOTSUP. */
  readonly sync?: WorkerConnectSpec;
  /** Called by the runner when the host is no longer needed. */
  close(): Promise<void>;
}

localHost

/**
 * The runner's own machine. Its `fs` is the vfs API over the real file system (so the sync
 * methods work), and its processes are children of the runner. Nothing to connect or close.
 */
export function localHost(options?: LocalHostOptions): Host;

LocalHostOptions

export interface LocalHostOptions {
  /** The environment processes start from. Defaults to the runner's own. */
  readonly env?: Readonly<Record<string, string | undefined>>;
  /** Default agent cwd. Defaults to the runner's working directory. */
  readonly workspace?: string;
}

nodeTransport

/**
 * Runs processes on the machine the runner is on. `baseEnv` is the environment every process
 * starts from; the `env` of a spawn is layered over it.
 */
export function nodeTransport(
  baseEnv: Readonly<Record<string, string | undefined>>
): ProcessTransport;

ProcessTransport

/** The one primitive a host must supply: spawn a process with byte streams. */
export interface ProcessTransport {
  /** Owns process-group cleanup and disconnect supervision; no outer watchdog is needed. */
  readonly supervised?: boolean;
  spawn(
    argv: readonly string[],
    options: {
      readonly cwd?: string;
      readonly env?: Record<string, string>;
    }
  ): Promise<HostProcess>;
}

SftpProvider

/**
 * A file system provider over an SFTP session. Async only: the sync methods throw `ENOTSUP`
 * unless the host brings a sync bridge (see `HostConnection.sync`). The session opens on first
 * use.
 */
export class SftpProvider extends VirtualProvider {
  #private;
  constructor(open: () => Promise<SFTPWrapper>);
  get supportsSymlinks(): boolean;
  stat(path: string): Promise<VirtualStats>;
  lstat(path: string): Promise<VirtualStats>;
  readdir(path: string, options?: ReaddirOptions): Promise<string[] | VirtualDirent[]>;
  mkdir(path: string, options?: MkdirOptions): Promise<string | undefined>;
  rmdir(path: string): Promise<void>;
  unlink(path: string): Promise<void>;
  rename(oldPath: string, newPath: string): Promise<void>;
  readFile(path: string, options?: Encoding): Promise<Buffer | string>;
  writeFile(path: string, data: string | Buffer, options?: WriteOptions): Promise<void>;
  appendFile(path: string, data: string | Buffer, options?: WriteOptions): Promise<void>;
  exists(path: string): Promise<boolean>;
  copyFile(src: string, dest: string): Promise<void>;
  realpath(path: string): Promise<string>;
  access(path: string): Promise<void>;
  readlink(path: string): Promise<string>;
  symlink(target: string, path: string): Promise<void>;
  openSync(path: string): unknown;
  statSync(path: string): VirtualStats;
  lstatSync(path: string): VirtualStats;
  readdirSync(path: string): string[] | VirtualDirent[];
  mkdirSync(path: string): string | undefined;
  rmdirSync(path: string): void;
  unlinkSync(path: string): void;
  renameSync(oldPath: string): void;
  readFileSync(path: string): Buffer | string;
  writeFileSync(path: string): void;
  appendFileSync(path: string): void;
  existsSync(path: string): boolean;
  copyFileSync(src: string): void;
  realpathSync(path: string): string;
  accessSync(path: string): void;
  readlinkSync(path: string): string;
  symlinkSync(_target: string, path: string): void;
}

SshAuth

export interface SshAuth {
  readonly username: string;
  /** An OpenSSH or PEM private key. */
  readonly privateKey: string;
  /**
   * The pinned host key fingerprint (`SHA256:…`, as `ssh-keygen -lf` prints it). Omit it only
   * when `stream` is already authenticated end to end.
   */
  readonly hostKey?: string;
}

sshConnect

/**
 * SSH (`ssh2`) over any byte stream, with keepalives. Authenticates with a private key and checks
 * the host key against the pinned fingerprint before anything is sent. What goes wrong after the
 * handshake is said to `log`: the first error as a warning, any later one as debug output.
 */
export function sshConnect(
  stream: ByteStream,
  auth: SshAuth,
  log?: PlugLogger
): Promise<SshConnection>;

SshConnection

export interface SshConnection extends AsyncDisposable {
  readonly transport: ProcessTransport;
  sftp(): VirtualProvider;
  forward(spec: ForwardSpec): Promise<AsyncDisposable>;
}

tcpStream

/** A TCP connection as a `ByteStream`. Rejects if the connection cannot be made. */
export function tcpStream(host: string, port: number): Promise<ByteStream>;

webSocketStream

/**
 * A WebSocket as a `ByteStream`, for gateways and HTTPS-only endpoints. Binary messages are the
 * bytes of the stream; text messages are rejected.
 */
export function webSocketStream(
  url: string,
  options?: {
    readonly headers?: Record<string, string>;
  }
): Promise<ByteStream>;

WorkerConnectSpec

/**
 * Where a worker thread can import the plug's `connect` function from. The sync fs bridge
 * opens its own connection there: the main thread is blocked in `Atomics.wait` during a sync
 * call and cannot drive a socket.
 */
export interface WorkerConnectSpec {
  readonly module: string;
  readonly exportName: string;
  /** Structured-clone-able arguments for `connect`. */
  readonly args: unknown;
}

wrapHost

/**
 * A host that runs every process of `base` through `wrap` (for example under an OS-level
 * sandbox). `fs`, `info` and `forward` are `base`'s, unchanged. It owns nothing: the plug that
 * created `base` closes it.
 */
export function wrapHost(base: Host, wrap: WrapProcess): Host;

WrapProcess

export type WrapProcess = (
  argv: readonly string[],
  options: {
    readonly cwd: string;
    readonly env: Record<string, string>;
  }
) => Promise<{
  argv: readonly string[];
  env: Record<string, string>;
}>;

On this page