Skip to main content

Headless Extension Connect

The Anima browser extension links an agent to a real browser so it can autofill vault credentials and drive logins without any secret entering the model’s context. Normally a person installs the extension and clicks Connect. For automation — a Puppeteer worker, a scheduled scraper, a partner integration — there is no person to click. Headless connect closes that gap: mint a one-time connect URL with the API, open it in the headless browser, and the extension binds its bridge to the agent automatically.
The connect response never contains a token or a secret. connectUrl itself carries a short-lived, single-use exchange code; the extension redeems it out of band. Nothing sensitive passes through the model.

The flow

1

Mint a connect URL

Call POST /v1/extension/connect. The API returns a connectUrl pointing at the console connect page, carrying a single-use exc_auto_… exchange code.
2

Open it in the headless browser

Navigate the automation browser (with the Anima extension loaded) to connectUrl. The exc_auto_ prefix tells the page to relay the code to the extension automatically — no click.
3

Wait for the bridge

The page exposes data-connect-status, which transitions to connected once the extension has bound its bridge to the agent. Wait on [data-connect-status="connected"], then run your automation.

Auth model

Who you authenticate as decides whether you pass agentId:
CredentialagentIdBehavior
Master key (mk_)RequiredSelects which agent the extension connects as.
Agent key (ak_)OmitThe server resolves the agent from the key. Passing another agent’s id is rejected.
Two org policies support headless connect: session and pre_approved. The prompt_owner policy is not supported — there is no owner to approve an unattended connection, so the API rejects it:
Headless connect is not available under the prompt_owner policy — use session or pre_approved.
Under pre_approved, the target agent must be on the org’s pre-approved allowlist (configure it under Settings → Extension in the console), or the request is rejected.

ttl

ttl is optional and shorten-only: "15m" | "1h" | "session". It may only be shorter than — or equal to — the org’s configured maximum. A value longer than the org policy is rejected rather than silently downgraded, so you learn the org forbids that lifetime instead of quietly getting a shorter one. Omit ttl to use the org default. A "session" TTL has no wall-clock expiry, so expiresAt comes back null. The separate exchangeExpiresAt is the deadline for redeeming the connect URL itself (a single-use handoff window, ~60 seconds) — distinct from expiresAt, the lifetime of the resulting connection.

Mint a connect URL

curl -X POST https://api.useanima.sh/v1/extension/connect \
  -H "Authorization: Bearer $ANIMA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agentId": "agent_123", "ttl": "session"}'
# → {
#     "agentId": "agent_123",
#     "connectUrl": "https://console.useanima.sh/extension/connect?code=exc_auto_…",
#     "expiresAt": null,
#     "exchangeExpiresAt": "2026-01-01T00:01:00.000Z",
#     "policy": "session"
#   }

Response

FieldTypeDescription
agentIdstringThe agent the extension will connect as.
connectUrlstringURL to open in the headless browser. Carries the single-use exc_auto_… code.
expiresAtstring | nullWhen the resulting connection expires. null for a session TTL.
exchangeExpiresAtstringDeadline to redeem connectUrl (single-use handoff window).
policy"session" | "pre_approved"The policy governing the session.

Puppeteer worker recipe

End to end: mint the URL, launch Chrome with the automation bundle loaded, open the URL, and wait for the bridge.
import puppeteer from "puppeteer";
import { Anima } from "@anima-labs/sdk";

const anima = new Anima({ apiKey: process.env.ANIMA_API_KEY });
const { connectUrl } = await anima.extension.connect({ agentId: "agent_123" });

// The automation bundle: the extension built with the Chrome Web Store `key`
// injected, so an unpacked --load-extension resolves to the SAME extension ID
// the connect page targets. Built with `bun run build:automation` → dist-automation/.
const EXT = "/path/to/anima-extension/dist-automation";

const browser = await puppeteer.launch({
  headless: "new", // old headless can't load extensions
  args: [
    `--disable-extensions-except=${EXT}`,
    `--load-extension=${EXT}`,
  ],
});

const page = await browser.newPage();
await page.goto(connectUrl);                 // exc_auto_ code auto-relays, no click
await page.waitForSelector('[data-connect-status="connected"]');
// The extension bridge is now live for agent_123 — drive your automation here.
Use the automation bundle, not the plain build. The console connect page only ever messages the published Chrome Web Store extension ID — it never trusts a caller-supplied one. An ordinary unpacked build gets a different, per-path ID, so the relay silently never reaches it. The automation bundle (dist-automation/) injects the Chrome Web Store key into the manifest, pinning the unpacked extension to the production ID the page targets.
A few operational notes:
  • --headless=new is required. Chrome’s old headless mode cannot load extensions at all.
  • A fresh browser profile connects with no prompt. The exc_auto_ code auto-relays on page load.
  • Re-connecting a profile that already holds a live bridge requires confirmation. An unattended page must not silently replace an existing connection — the extension asks the owner to approve the re-pair, and denies otherwise. For a clean worker run, start from a fresh profile.
  • Vault — Browser Extension — how the extension performs credential autofill without exposing secrets.
  • Vault — per-agent encrypted credential storage and the secret boundary.
  • Security — key types (mk_ vs ak_) and the master-key model.