# Integration Guide — zero to first task

This is the developer front door — for a product that wants to run a browser action for
its users. It walks you from nothing to one real action (here: a document upload into a
third-party site) in your end user's own browser. ~30 minutes. The example below uses a
site with no API, but the same flow works for any browser process.

> **Roles recap.** *You* (the customer) supply the app + the signed playbooks. *Your end
> user* supplies the real browser + their login for the destination site. *OpenErrand* is
> the pipe. Nobody but the end user ever needs the destination credentials, and
> **credentials never touch OpenErrand's servers**. (Documents are intended to be sourced
> locally too — see the note in *What flows where* below.)

## 0. Sign up and register your signing key

[Sign up](/signup.html) (or `POST /signup` with your email) to get, on the spot:
- a **tenant ID** (e.g. `acme`);
- a **tenant API key** — your server-side secret (request pairing tokens, run tasks, read audit).

Then generate your signing keypair and register the public half (you keep the private key):

```bash
npx @obep/cli keygen --out keys     # -> keys/tenant.pub, keys/tenant.key (keep secret)
curl -X POST $RELAY/signing-key \
  -H "Authorization: Bearer $API_KEY" -H "content-type: application/json" \
  -d "{\"publicKey\":\"$(cat keys/tenant.pub)\"}"
```

Tasks run against the managed **relay endpoint** (`wss://relay.openerrand.app`) — or
self-host ([SELF_HOSTING.md](./SELF_HOSTING.md)).

## 1. Install

```bash
npm install @obep/sdk        # the client your app integrates
npm install -D @obep/cli     # the obep-playbook authoring CLI (or use npx)
```

## 2. Author a playbook for the portal

A **playbook** is a signed, permission-fenced recipe for one portal flow. Most teams have
their **coding assistant** draft it — see [Author a playbook with an LLM coding
tool](./PLAYBOOK_AUTHORING.md). You can also **record it once**:

```bash
# In the OpenErrand side panel: "Record a playbook" -> do the flow -> "Stop & derive".
# Or derive from an observed-run trace:
npx @obep/cli derive portal-trace.json --out portal.json   # steps + tightest fence; secrets never captured
npx @obep/cli lint   portal.json                              # schema + danger flags
npx @obep/cli sign   portal.json --key keys/tenant.key --out portal-signed.json
```

Register `portal-signed.json` with the relay (via OpenErrand, or your own registry if
self-hosting). It now has a `playbookId` you reference at runtime.

## 3. Pair the user's browser to (tenant, user)

When the user logs into your app, your **server** mints a short-lived pairing token:

```ts
// your server, authenticated with your API key
const res = await fetch(`${RELAY_HTTP}/pairing-tokens`, {
  method: "POST",
  headers: { authorization: `Bearer ${API_KEY}`, "content-type": "application/json" },
  body: JSON.stringify({ userId: "dana" }),
});
const { pairingToken } = await res.json();   // -> binding (acme, dana) once approved
```

**One-click handoff (recommended).** Your app's page hands the token to the extension
directly, so the user just clicks **Connect** in your app and **Approve** in the
extension — no copy/paste. Add your origin to the extension's `externally_connectable`
list, then from your page:

```js
const OPENERRAND_EXTENSION_ID = "<published-extension-id>";
function connectOpenErrand(pairingToken) {
  if (!window.chrome?.runtime?.sendMessage) return false; // extension not installed / origin not allowlisted
  chrome.runtime.sendMessage(
    OPENERRAND_EXTENSION_ID,
    { type: "openerrand-pair", pairingToken, appName: "Acme" },
    (resp) => { if (resp?.received) toast("Approve the connection in OpenErrand"); },
  );
  return true;
}
```

The extension parks the request and shows the user **“Allow Acme to run tasks in your
browser?”** — it never pairs silently. The token is single-use and relay-validated, the
origin must be allowlisted, and the user approves: three independent gates.

If your page can't do the handoff, the user can paste the token into the side panel's
**Connect an app** field as a fallback. Enterprise installs auto-pair via a tenant-signed
identity assertion — see [ENTERPRISE_DEPLOYMENT.md](./ENTERPRISE_DEPLOYMENT.md).

## 4. The user stores their portal login (once)

In the OpenErrand side panel the user unlocks their vault (passphrase) and saves their carrier
credentials. These are AES-GCM encrypted on their device, namespaced to the binding, and
**never** sent to you or OpenErrand.

## 5. Run the task

From your **backend** (the API key and your LLM stay server-side; the work happens in the
user's browser):

```ts
import { RelayClient } from "@obep/sdk";
import { WebSocket } from "ws"; // Node; in the browser, omit WebSocketImpl

const client = new RelayClient({ url: RELAY_WS, apiKey: API_KEY, WebSocketImpl: WebSocket });

const run = client.run({
  url: "https://portal.example.com/login",
  userId: "dana",
  playbookId: "acme.portal-upload",
  decide,                       // your LLM — only used if a deterministic step breaks
});

for await (const status of run) updateUserUI(status.phase);   // live status stream
const { confirmationNumber } = await run;                     // result bag
```

A deterministic playbook needs **no LLM** for the happy path — the steps drive it, and no
page content leaves the device. To back `decide` with a real model (Claude or otherwise)
for the cold-start / fallback path, see the [LLM decider quickstart](./LLM_DECIDER.md).

## 6. See what happened

```bash
curl -H "Authorization: Bearer $API_KEY" $RELAY_HTTP/audit/acme    # your tenant only (cross-tenant = 403)
```

Or use the OpenErrand dashboard. Audit records log *that* an action occurred (domain +
hash, never content).

## What flows where (the part your security team will ask about)

| Data | Path |
|---|---|
| Portal **credentials** | on-device vault → straight to the portal. Never to you or OpenErrand. |
| The **document** | the user's browser → the portal. (Source it locally; don't route it through the relay.) |
| **Page context** (LLM mode only) | minimized + redacted → your LLM, via the relay (which forwards, never stores). Deterministic mode sends none. |
| **Status + audit** | relay → your app; audit is per-tenant and access-controlled. |

Verify all of this yourself: read the open extension source and run the
[conformance suite](../obep/conformance/) against the relay. See
[SECURITY_MODEL.md](./SECURITY_MODEL.md).

## Next

- [Security model](./SECURITY_MODEL.md) · [Error taxonomy](./ERROR_TAXONOMY.md) ·
  [Self-hosting](./SELF_HOSTING.md) · [Enterprise deployment](./ENTERPRISE_DEPLOYMENT.md)
- Sample playbooks: [`examples/playbooks/`](../examples/playbooks/)
