branch:
host-bridge.ts
3034 bytesRaw
/**
 * HostBridgeLoopback — a WorkerEntrypoint that provides controlled workspace
 * access to extension Workers loaded via WorkerLoader.
 *
 * This is a loopback: the extension worker's `env.host` binding points here,
 * and each method call resolves the parent agent via `ctx.exports`, then
 * delegates to the agent's workspace proxy methods (`_hostReadFile`, etc.).
 *
 * Props carry serializable identifiers (agent class name, agent ID, and
 * permissions) so the binding survives across requests and hibernation.
 *
 * Users must re-export this class from their worker entry point:
 *
 * ```typescript
 * export { HostBridgeLoopback } from "@cloudflare/think/extensions";
 * ```
 *
 * @experimental Requires the `"experimental"` compatibility flag.
 */

import { WorkerEntrypoint } from "cloudflare:workers";
import type { ExtensionPermissions } from "./types";

export type HostBridgeLoopbackProps = {
  agentClassName: string;
  agentId: string;
  permissions: ExtensionPermissions;
};

export class HostBridgeLoopback extends WorkerEntrypoint<
  Record<string, unknown>,
  HostBridgeLoopbackProps
> {
  private _permissions = this.ctx.props.permissions;

  private _getAgent() {
    const { agentClassName, agentId } = this.ctx.props;
    // @ts-expect-error — experimental: ctx.exports on WorkerEntrypoint
    const ns = this.ctx.exports[agentClassName] as DurableObjectNamespace;
    return ns.get(ns.idFromString(agentId));
  }

  #requirePermission(level: "read" | "read-write"): void {
    const ws = this._permissions.workspace ?? "none";
    if (ws === "none") {
      throw new Error("Extension error: no workspace permission declared");
    }
    if (level === "read-write" && ws !== "read-write") {
      throw new Error(
        "Extension error: workspace write permission required, but only read granted"
      );
    }
  }

  async readFile(path: string): Promise<string | null> {
    this.#requirePermission("read");
    return (
      this._getAgent() as unknown as {
        _hostReadFile(path: string): Promise<string | null>;
      }
    )._hostReadFile(path);
  }

  async writeFile(path: string, content: string): Promise<void> {
    this.#requirePermission("read-write");
    return (
      this._getAgent() as unknown as {
        _hostWriteFile(path: string, content: string): Promise<void>;
      }
    )._hostWriteFile(path, content);
  }

  async deleteFile(path: string): Promise<boolean> {
    this.#requirePermission("read-write");
    return (
      this._getAgent() as unknown as {
        _hostDeleteFile(path: string): Promise<boolean>;
      }
    )._hostDeleteFile(path);
  }

  async listFiles(
    dir: string
  ): Promise<
    Array<{ name: string; type: string; size: number; path: string }>
  > {
    this.#requirePermission("read");
    return (
      this._getAgent() as unknown as {
        _hostListFiles(
          dir: string
        ): Promise<
          Array<{ name: string; type: string; size: number; path: string }>
        >;
      }
    )._hostListFiles(dir);
  }
}