# Canaster Programmatic Agent API

This is the canonical public guide served at `https://canaster.in/agent-api.md`.

## Discover and await the interface

Canaster advertises this guide through `llms.txt`, server-visible HTML metadata, and the visible accessible **Use window.canaster** link in the application toolbar. Do not click or type in the product UI, inspect the DOM or canvas for document state, access browser storage, or call Daptin directly.

The application accessibility tree also exposes a standalone **Canaster automation interface** note. Browser agents must use page-JavaScript execution to access `window.canaster`. An agent without page-JavaScript execution cannot automate Canaster and must not fall back to UI controls or canvas input.

`window.canaster` remains absent until the current application is ready. The page records its current lifecycle in `document.documentElement.dataset.canasterAgentStatus` and emits `canaster:ready`, `canaster:unavailable`, and `canaster:disposed` events. Attach listeners before checking the current state so readiness cannot race discovery:

```ts
function currentCanaster() {
  if (window.canaster) return Promise.resolve(window.canaster);
  return new Promise<NonNullable<typeof window.canaster>>((resolve, reject) => {
    const ready = () => {
      cleanup();
      if (window.canaster) resolve(window.canaster);
      else reject(new Error("Canaster reported ready without its agent interface."));
    };
    const unavailable = (event: Event) => {
      cleanup();
      const reason = (event as CustomEvent<{ reason?: string }>).detail?.reason ?? event.type.replace("canaster:", "");
      reject(new Error(`Canaster agent interface is unavailable: ${reason}`));
    };
    const cleanup = () => {
      window.removeEventListener("canaster:ready", ready);
      window.removeEventListener("canaster:unavailable", unavailable);
      window.removeEventListener("canaster:disposed", unavailable);
    };
    window.addEventListener("canaster:ready", ready);
    window.addEventListener("canaster:unavailable", unavailable);
    window.addEventListener("canaster:disposed", unavailable);
    if (window.canaster) ready();
    else if (["unavailable", "disposed"].includes(document.documentElement.dataset.canasterAgentStatus ?? "")) {
      unavailable(new Event(`canaster:${document.documentElement.dataset.canasterAgentStatus}`));
    }
  });
}

const api = await currentCanaster();
```

CLI agents use their browser controller's page-evaluation capability to run the same code inside the loaded Canaster page. There is no shell-only document-editing protocol.

Canaster exposes one document automation interface after the application is ready:

```ts
const api = window.canaster;
if (!api) throw new Error("Canaster is not ready");
console.log(api.protocolVersion, api.applicationInstanceId);
```

Browser-resident agents call this object directly. CLI-driven browser agents evaluate the same calls in the loaded page's JavaScript context. There is no separate CLI document-edit protocol, DOM-query path, canvas-input path, storage path, or direct Daptin path. `daptin-cli` remains the required maintenance interface for a running Daptin instance; it is not a substitute document editor.

The frozen `commands` object provides `list`, `query`, `preview`, `execute`, `batch`, `cancel`, and `changes`. Agents should discover descriptors with `list()` and read the exact target before mutation.

## Read and target an exact pane

```ts
const occurrences = api.commands.query({
  queryId: "document.occurrences",
  page: { limit: 100 }
});
if (occurrences.status !== "succeeded") throw occurrences.error;

const panePath = occurrences.page.items.find((item) => item.kind === "pane").path;
const pane = api.commands.query({ queryId: "pane.read", path: panePath });
const body = pane.page.items[0];
```

An object or surface id is not an occurrence identity. Reuse the complete returned path, including its mount chain. Paginate with the opaque `nextCursor`; a stale cursor requires a new first page.

## Revision-fenced mutation

```ts
const result = await api.commands.execute({
  requestId: "agent-edit-1",
  idempotencyKey: "edit-section-a",
  commandId: "pane.text.replace-range",
  path: body.path,
  expectedDocumentRevision: pane.revisions.document,
  expectedBodyRevision: body.revision,
  input: { from: 0, to: 5, expected: "Hello", insert: "Welcome" }
});
```

Every query, command, and batch reports document/runtime revisions. A mismatched optimistic fence returns `stale-revision` before effects. Pane mutations additionally require `expectedBodyRevision`. Identical reuse of an idempotency key returns the original immutable result; use with different input returns `idempotency-conflict`. The per-application replay window retains 128 keys.

`pane.body.replace` replaces the complete validated props for any non-Portal built-in. Prefer implementation-owned incremental commands listed by `pane.read` when they express the intended edit. Portal configuration is performed by creating a Portal and continuing with the exact child-surface path returned by that command.

## Atomic body batch

```ts
const batch = await api.commands.batch({
  requestId: "agent-batch-1",
  idempotencyKey: "populate-two-panes",
  commands: [
    { commandId: "pane.body.replace", path: first.path, expectedBodyRevision: first.revision, input: { props: firstProps } },
    { commandId: "pane.body.replace", path: second.path, expectedBodyRevision: second.revision, input: { props: secondProps } }
  ]
});
```

A batch accepts 1–64 durable registry-owned pane-body commands on distinct bodies and commits them as one transaction and undo entry. Validation is complete before the transaction begins. Blob/asset staging, transfer, online, transient media, runtime-only, and other non-transactional commands are ineligible; a mixed batch changes nothing.

## Runtime and presentation commands

Runtime, history, pane geometry, camera, and presentation commands use the same `execute` envelope and exact targets as document commands. `presentation.exit` accepts `{ reason: "back" | "workspace" }`; omit the input to use `back`. The reason preserves the requested breadcrumb semantic and does not create document history.

## Assets

Pass live `Blob` values without serializing them:

```ts
const imported = await api.commands.execute({
  requestId: "agent-image-1",
  commandId: "asset.import",
  target: { kind: "surface", path: surfacePath },
  input: { content: imageBlob, mediaIntent: "image" }
});
```

Use `document.assets` for safe metadata, `asset.inspect` for current availability, `asset.export` for bounded verified bytes, and transfer commands for pane packages. Storage keys, object URLs, filenames, credentials, and backend reference identities are not exposed. Imports use canonical staging, validation, promotion, transaction, and compensation owners.

When `asset.import` or `transfer.import` targets a pane replacement, pass the `revision` returned by `pane.read` as `expectedBodyRevision`. Canaster preserves that caller fence through staging and rejects the replacement if the exact occurrence or body revision changes before commit. Surface-target imports do not use a body revision.

## Cancellation and changes

```ts
const pending = api.commands.execute({ requestId: "asset-work-7", commandId: "asset.import", target, input });
api.commands.cancel("asset-work-7");
const terminal = await pending;

let cursor = api.commands.changes().cursor;
const next = api.commands.changes({ cursor, limit: 100 });
```

Cancellation is available for asynchronous action preparation and abort-aware asset/transfer work. Non-cancellable commands are not advertised as active requests. The bounded 256-entry change feed reports safe document/runtime/online changes, operation kinds, transaction ids where relevant, identities, and revisions. `cursor-overflow` means the caller must take fresh queries before continuing.

## Online lifecycle and security

The same `execute` method owns authorized workbook list, open, create, save, rename, URL, and conflict-resolution commands. Stable routes use `/d/:username/:slug`; current-view locators carry no authority. Responses omit backend references, account subjects, tokens, permissions rows, and raw Daptin records. Successful save acknowledgement is based on the canonical readback path.

Visibility and group-role mutation are intentionally unavailable until the upstream atomic document/preview/source-asset access mutation required by the current online-sharing program is released and qualified. Agents must not replace that missing capability with direct HTTP, storage access, or client-side fan-out.

Treat `blocked`, `partial`, and `failed` as terminal structured outcomes, not success. After reload, compare `applicationInstanceId`; retained calls belong to the disposed prior application and fail. Never infer authority from a path or locator, and never log document content, binary payloads, credentials, or backend identities.
