Docs/Integration Guide — zero to first task raw .md

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 errands. 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 (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):

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).

#1. Install

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

#2. Author an errand for the portal

A errand is a signed, permission-fenced recipe for one portal flow. The easiest way to make one is to record it once — do the flow in your real browser and the extension captures every step with its selectors and derives the errand for you. No screenshots, no transcribing, no LLM:

OpenErrand side panel → Advanced → Record an errand
  1. paste the start URL, click ● Record
  2. do the flow once (log in, click, fill, upload, …)
  3. click ■ Stop & derive  →  the finished errand JSON appears

Then lint + sign it with the CLI (the human holds the signing key):

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 an errandId you reference at runtime.

Can't record the flow (e.g. scripting against a site you don't have open)? Your coding assistant can hand-write the JSON instead — see Author an errand with an LLM coding tool. Recording is preferred whenever it's an option: it gets the selectors right that an assistant would have to guess at.

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

You do not write any connect UI or JavaScript — OpenErrand ships a hosted button that owns the whole experience. There are two pieces: one backend route you provide, and one <script> tag you drop in.

1. Your server mints a pairing token (the one thing OpenErrand can't do for you — the API key is a server secret that can't reach the browser). Expose it as a route the widget can call, e.g. POST /openerrand/pairing-token:

// your server, authenticated with your API key
app.post("/openerrand/pairing-token", requireAuth, async (req, res) => {
  const r = await fetch(`${RELAY_HTTP}/pairing-tokens`, {
    method: "POST",
    headers: { authorization: `Bearer ${API_KEY}`, "content-type": "application/json" },
    body: JSON.stringify({ userId: req.user.id }),   // -> binding (acme, this user) once approved
  });
  res.json({ pairingToken: (await r.json()).pairingToken });
});

2. Drop in OpenErrand's connect button. One script tag and a container — no connect JS:

<script src="https://openerrand.app/embed.js"
        data-openerrand-token-url="/openerrand/pairing-token"
        data-openerrand-tenant="acme"></script>
<div data-openerrand-connect></div>

Bearer / localStorage auth? The default above sends the session cookie to your token route. If your app authenticates with a Bearer token (no cookie — common with client-side Supabase), drop data-openerrand-token-url and define window.openerrandToken() to mint a token however you authenticate; the widget still owns the button, popup, and status:

<script>
  window.openerrandToken = async () => {
    const { data: { session } } = await supabase.auth.getSession();
    const r = await fetch("/openerrand/pairing-token", {
      method: "POST", headers: { authorization: `Bearer ${session.access_token}` },
    });
    return (await r.json()).pairingToken;
  };
</script>

The widget renders the button, opens a small popup window (never a full-page redirect), runs the handoff, and shows “Connect OpenErrand” vs “OpenErrand Connected” on its own. Under the hood the popup — on openerrand.app, the only origin allowed to talk to the extension — looks up whose token this is (via POST /pairing-tokens/describe, read-only) and hands it to the extension, which parks the request and shows the user “Allow Acme to run tasks in your browser?” — it never pairs silently. The connected state is read from the extension itself (a hidden openerrand.app status frame), so no extra backend. Three independent gates hold: the token is single-use and relay-validated, the handoff comes from the trusted openerrand.app origin, and the user approves.

Your coding assistant can generate the token route + the <script> tag with the scaffold_web_pairing MCP tool. For the full flow, the connect states, and the security gates, see Pairing.

If the extension isn't installed, the connect page shows an install prompt. As a fallback, the user can paste the token into the side panel's Connect an app field. Enterprise installs auto-pair via a tenant-signed identity assertion — see 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):

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",
  errandId: "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 errand 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.

#When a run fails: the run trace

The terminal status (and the awaited result) carries a structured per-step trace, so you can see exactly which step failed and why without reproducing the run:

  • trace — every executed step as { i, action, ok, code } (code is set on the one that failed)
  • failedStep — the i of the failing step (on error)
  • finalUrl — the tab's last URL when the run ended

An errored run rejects the awaited result with a RunError exposing these:

import { RunError } from "@obep/sdk";

try {
  const { confirmationNumber } = await run;
} catch (e) {
  if (e instanceof RunError) {
    console.error(`failed at step ${e.failedStep} [${e.code}] on ${e.finalUrl}`);
    console.error(e.trace); // [{ i:0, action:"navigate", ok:true }, { i:1, action:"fill", ok:false, code:"element_not_found" }]
  }
}

The same fields are on every status you iterate. Note: lint_errand / npx @obep/cli lint validate schema only — they can't tell whether a selector matches the live page. The trace is how you debug the actual run.

#6. See what happened

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 against the relay. See SECURITY_MODEL.md.

#Where things live (which package does what)

The repo has two trees. obep/ is canonical for everything you run against; openerrand/ is the managed service that consumes it.

You want to… Package Notes
Integrate the client (RelayClient, RunError) @obep/sdk (obep/sdk) what your app imports
Author/sign/lint errands from an AI tool @openerrand/mcp (openerrand/mcp) the MCP server; errand_guide + errand_capabilities
Understand the wire/playbook types @obep/protocol (obep/protocol) StatusMessage, Playbook, PLAYBOOK_SCHEMA, FileInput
Know what lint checks @obep/enforcement (obep/enforcement) schema + danger flags (not behavior)
Know what executes steps obep/extension the Chrome extension runner
Run the relay obep/relay-reference (core) / openerrand/relay-service (managed) managed reuses the reference router

The MCP's guide + scaffolds are fetched from the hosted source (openerrand.app/mcp/*.txt) with the package's bundled content/ as the offline fallback — a single source (openerrand/mcp/content).

#Next