Skip to content

Record a flow in Chrome, narrated, straight into Intelligence with the OpenBot Recorder - #809

Draft
davidmckayv wants to merge 278 commits into
mainfrom
david/recorder-extension
Draft

davidmckayv wants to merge 278 commits into
mainfrom
david/recorder-extension

Conversation

@davidmckayv

@davidmckayv davidmckayv commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

Adds the OpenBot Recorder, a Manifest V3 extension for Chrome and Edge that captures a task from any site. A person clicks Record, does the task in their own browser while talking it through, and clicks Stop. The recording goes straight from the browser into CopilotKit Intelligence as a Product Trajectory, in that person's own Learning space, where Learning can turn it into a Skill.

A recording is the person's steps, not the page's traffic:

  • Each action is one step: a click, a change (with its masked value), a select, a submit, an Enter, or a top-frame page change.
  • Each step carries what's needed to find that element again: role and name, visible text, input type, a ranked list of stable selectors, frame path, page URL and title, and the element's box with the viewport size. A click on a container resolves to the button or link it was meant for, and automatic redirect chains collapse into one page change.
  • The person's narration is transcribed by the browser's built-in speech recognition. Each phrase becomes a transcript event with its start time, and no audio leaves the extension or is stored.
  • No standalone network events. A step carries at most the one request it sent when it saved something: a write the step caused that carries the values just typed, names a write-style operation, or is a GraphQL mutation. Telemetry, polling, presence pings, read receipts and the page's own reads are never recorded.
  • Masking happens in the browser, before anything leaves. It covers passwords, card numbers, and one-time and 2FA codes (including code fields with no autocomplete hint, and codes typed one digit per box). It also covers credential headers, tokens in URLs and bodies, and spoken card numbers.

OpenBot receives nothing that was recorded. It does three things:

  • Connects a browser. Settings > Browsers > Connect this browser exchanges a one-time code for an extension-only token. The token is stored hashed, works only on the Recorder's routes, and can be revoked from the same list. OpenBot Desktop can connect the Recorder on its own computer through a native messaging host.
  • Hands out the capture pass. The runtime's trajectory/<id>/connect answers only the Recorder's bearer.
  • Puts the trajectory in the person's own Learning space. OpenBot binds the trajectory to its person and returns that space (user- plus a hash of the OpenBot user id) to the runtime's Learning selector. It creates the space when the browser connects, and again on a capture pass if it is still missing.

Learning, per person, shared by choice:

  • Every Bot a person uses draws Skills from three places: its own Skills, the person's own, and any Skills others have shared. On a name clash the Bot's own Skill wins, then the person's own. Shared Skills from two people stay distinct.
  • Settings > Learning offers three choices: only my Bots (the default), everyone's Bots, or pick individual Skills to share. Each change is audited.
  • A missing or slow personal space never fails a run.

Permissions are asked at install, so the first Record records. Website access is a required host permission, and a fresh install opens one setup tab that asks for the microphone. The install dialog shows the same warnings the debugger permission already implies.

Also in this branch:

  • @copilotkit/runtime moves to 1.79.1 (AG-UI 1.0.2), for server-side Learning space assignment.
  • Migration 0057 adds the Recorder and Learning sharing tables.
  • The Bot's cloud browser and the extension share one copy of the labelling and masking rules, in shared/.

Intelligence needs settings only, no code:

  • a licence that includes trajectories;
  • FF_LEARNING_TRAJECTORIES on;
  • an INTELLIGENCE_GATEWAY_WS_URL that people's browsers can reach;
  • a credential that can create Learning containers: INTELLIGENCE_SERVICE_TOKEN with INTELLIGENCE_PROJECT_ID on a local evaluation install, or a CopilotKit CLI login on managed Intelligence. The runtime key alone cannot create a person's space.

Where it runs

  • New state that outlives a request? Postgres, in migration 0057:

    • recorder_connections: hashed bearer, label, revoked flag.
    • recorder_connect_codes: hashed one-time codes, deleted on use or expiry.
    • recorder_trajectory_bindings: trajectory to person.
    • learning_person_spaces: each person's Learning space.
    • learning_skill_sharing: each person's sharing mode.
    • learning_open_skills: the individual Skills a person chose to share.

    Four per-process caches have TTLs and hold nothing that must be shared:

    • the organization sign-in verdict;
    • the Learning-space check;
    • refusal-audit de-duplication;
    • the list of shared Skills, refreshed at once on the replica that saved a change and within 30 s elsewhere.
  • What happens on the second replica?

    • A connect code minted on one replica is exchanged on another through Postgres.
    • Any replica checks a bearer against its hashed row.
    • Two replicas creating the same personal space both succeed: Intelligence answers "already exists" to the second.
    • A capture pass for the same trajectory binds once: another person gets a 404.
    • A sharing change reaches other replicas within 30 s.
  • Anything serialised?

    • Code exchange is a delete inside a transaction, so a code is used once.
    • Binding is an insert on the trajectory_id primary key with ON CONFLICT DO NOTHING, followed by an owner check.
    • Container creation relies on Intelligence's unique container id.
    • The cap of 10 live connect codes per person is a count-then-insert. It is a spam bound, not a security boundary: concurrent requests can exceed it by the number in flight.
  • Anything fanned out to a browser? No. The extension streams to Intelligence's realtime gateway directly.

  • New listener, port, or schedule? No new port and no schedule.

    • The routes are under /api/recorder/* and /api/settings/learning, behind the same ingress as the API.
    • The native messaging host is a Desktop process started by the browser on the person's own computer.

Boundary and audit

  • Every acting call still goes through the gateway. The Recorder bearer cannot run a Bot or read a conversation. It reaches only the Recorder routes and the trajectory connect route, and the gate resolves the person before the pass is minted.
  • New refusals and failures each write a row:
    • recorder.connected, recorder.revoked and recorder.trajectory_refused;
    • learning.sharing_changed, learning.skill_opened and learning.skill_closed.
  • Nothing new is trusted from the client that the server can resolve itself.
    • The person comes from the bearer.
    • The Learning space is derived on the server from the person.
    • The extension trusts only the origin Chrome reports for externally_connectable messages.

Changelog

  • CHANGELOG.md under Unreleased. It covers:
    • the Recorder;
    • Settings > Browsers and Settings > Learning;
    • the capture pass and its audit events;
    • migration 0057;
    • OPENBOT_RECORDER_EXTRA_EXTENSION_IDS (a bad value stops the server at boot);
    • the runtime 1.79.1 move to Chat Completions for openai/* models behind a non-OpenAI OPENAI_BASE_URL (COPILOTKIT_OPENAI_API=responses keeps the Responses API).

Proof

Run against a real local Intelligence (v0.13.14), in real Chrome 155 and real Edge 155, on fixture sites and real sites, including a person's own narrated sessions in a real SaaS app.

  • Steps, not traffic. Replayed through the new rules, two real sessions shrink from 693 and 525 events to 55 or fewer each, with 0 network events. They read as what the person did: sign in, enter the 2FA code, open contacts, open a contact, ask the assistant (its sendMessage attached), close banners (each "mark as read" attached).
  • Real Chrome. On a fixture full of polls, beacons, trace exports, prefetches and iframe traffic, the trajectory held only steps, with each form's save attached to its submit or Enter.
  • Masking. I searched full dumps of Intelligence's and OpenBot's databases for every test secret, in raw, spaced and form-encoded forms: passwords, card numbers, tokens and 2FA codes (single-field and one digit per box). Every secret had 0 hits, on both capture paths. On the build before the 2FA fixes, the same runs leaked the code in 2 to 3 events.
  • Voice. Narration came back as transcript events between the steps, and a spoken card number was stored as [redacted], including when the recognizer dropped digits.
  • Learning. A recording joined the person's own space (user-...), active, in cpki.trajectory_container_memberships. Tests prove the Skill merge, clash rules, sharing modes and fail-soft, because Learning delivery does not run locally.
  • Lifecycle:
    • reconnecting mid-recording keeps it going;
    • Disconnect ends it failed:disconnected;
    • a revoke in Settings > Browsers stops it within 30 s;
    • a browser restart sends what was kept;
    • a fresh install in Chrome and Edge opens one setup tab, Connect needs no prompt, and the first Record records.
  • Checks:
    • format:check, lint, typecheck, build, drizzle-kit check and drift: all pass.
    • test:ci: 8,420 tests, 0 failures.
    • Desktop: cargo fmt, clippy -D warnings and the lib and native host tests pass.

Known limits:

  • A click inside a sandboxed iframe the extension cannot enter is not recorded.
  • Voice in Edge with a real microphone is unverified. With a fake microphone, Edge's built-in recognition returned empty phrases, and the popup says so.

…the release tree, so the check accepts the Recorder extension's package.json and still fails when a workspace's manifest is missing
… shared length, the longest that fits, so the larger body is cut first and a small response survives a large upload whole instead of being emptied before the request is touched
…er instead of the offscreen document calling chrome.runtime.getManifest, which Chrome does not give an offscreen document, so kept recordings are sent again after a reconnect or a browser restart; the chrome fake now gives each context its own chrome, the offscreen document only runtime messaging, getURL and id, and a new test runs the real service worker and document together through both paths
… still stands and end the recording through the sign-out path on a revoked or signed-out answer, so a browser revoked in OpenBot's Settings stops streaming within 30 s instead of at the next popup open or 12-hour keep-alive; an unreachable OpenBot or an error page keeps the recording going, and a restarted service worker resumes the checks
…n in ten minutes so an ordinary connect, reconnect, Disconnect and revoke run never meets it, and answer a connect over the limit as throttled with the time left instead of as busy, so Settings says to try again in so many minutes rather than that another connect is under way
… number or typed secret in spoken numbers, which speech recognition writes with commas, dashes or a currency sign, then runs the usual text masking
…he offscreen document opens the microphone and runs the browser's own speech recognition on it, each final phrase goes into the recording as a masked transcript event through the same outbox and stream, and Stop, Disconnect, a sign-out or a policy move ends it with the recording; OpenBot receives nothing of it
…itle says it is listening with the last words heard, or why it is not in a plain sentence, with a switch to mute it; the first Record opens a microphone page once to ask for the lasting grant the hidden document needs, and Record sends the page's language
…cript now redacts every spoken run of six digits or more whatever its checksum (a date said in full excepted), and every number after a word that names a secret (card, PIN, CVV, account, password and the like) however short, including in the phrase after it, since Chrome splits a phrase at a pause; the popup's interim words go through the same masking with the phrase before them, so a card number read aloud no longer reaches Intelligence or the Voice row as "4012-888888-1881" or "4012-8"
…no words: as Edge does, recognition starts, hears speech and raises no error while every final phrase comes back empty, so the row said Listening forever; now, 20 s after speech is first heard with no phrase in words, the row says this browser is not turning your speech into words and recording carries on without narration, keeps listening, and drops the sentence when a real phrase arrives
… Record records: website access is now a required host permission that the install grants (the debugger permission already implies every site, so Chrome and Edge show the same install warnings and an update is no privilege increase), and a fresh install opens one setup page that asks for the microphone as it loads, with a button to ask again and the answer in plain words, worded for Chrome's or Edge's prompt; the access page, the website-access prompts at Connect and at Record, and the microphone tab the first Record opened are gone, Record never opens a tab, and a recording without the microphone goes on while its Voice row says the microphone is not allowed and offers the setup page on a click
…s joins no Learning space: the popup said Learning may not see it whenever OpenBot could not check the Bot's space with Intelligence (Learning delivery off, its skills endpoint answering 404, or Intelligence not answering), although the pass still joins the Bot's container and the trajectory lands in it; one shared rule, joinedLearningContainer, now decides both which container the server's selector joins and when the popup warns, so a Bot with no space, a container Intelligence says does not exist, or settings OpenBot could not read still warn, and after Stop an unchecked space says the recording goes into the Bot's Learning space
…s under the sentence, as on the popup's Voice row, instead of squeezing it, the sentence keeps the muted colour the popup uses for the same state, and how to answer the prompt shows only while there is a prompt to answer
…one-time-code is now sensitive by its id, name, placeholder, label or aria-labelledby text, test id, class or short numeric shape (zip, postal, promo, coupon and country codes excepted), so its value, every attribute that echoes it and its digest are masked; verificationCode, otp, mfa_code, smsCode, authCode and like keys are credentials in bodies and URLs, and a bare code is redacted when it holds 4 to 10 digits or sits on a sign-in, verification or token request, as are code and state on an OAuth callback, so a recorded sign-in no longer sends its code to Intelligence in the input event or the request body
…s under one parent, wrapper or form (or numeric boxes side by side in one parent) is one sensitive code field though no box has a label, so every box's value is redacted in its input events, and the code the boxes make together, in order, is hashed on each input, after a paste the page spreads itself and when a box loses focus, so it is masked in any body; a lone numeric one-character box is sensitive on its own too
…harness catalogue paths and the Recorder's extension id file
…, XHR or top-frame document request that starts within a second of a click, edit, submit or key press in that tab, or that the response of such a call starts in turn (two links, three seconds at most), on both the debugger and the page-hook path; analytics, telemetry, ads and monitoring endpoints, prefetches, aborted calls, a call the tab already made (polling), and anything the page starts while the person does nothing are dropped before any body is read. The content script reports each action's time (never its detail) for this, and records a form submit and Enter in a field as their own events, masked like a click. Replayed on two real recordings of a CRM this halves the network events and drops every beacon, while the calls behind each click (a contact loading, a question sent to the assistant) stay.
…y, once per address: an iframe loading or rewriting its own address, and a page Chrome prerenders out of sight, are the page's doing and no longer recorded, and a single-page app setting the same address again and again records it once (a reload is recorded again, as is the reloaded page). On the two real CRM recordings this takes navigation events from 94 to 18 and from 47 to 11.
…rder: every recording a person makes joins their own Intelligence Learning container (user- and 32 hex digits of the SHA-256 of their OpenBot user id), made on first need with the credential that makes the openbot container (a service token on local evaluation, or a CLI login on managed Intelligence), when the browser connects and again on a capture pass, idempotently across replicas because Intelligence's container id is unique; the popup loses its Bot picker, the x-openbot-recorder-bot header, canRecordForBot, the no-Bot and Bot-forbidden refusals and /api/recorder/bots go, recorder_trajectory_bindings binds a trajectory to its person only (another person's second pass is not found), and the warning that Learning will not see a recording shows only when the person's space cannot be made or reached
…nd Settings, Learning shares them: a run gets the Bot's own learned Skills, then the person's own, then the Skills other people opened, the Bot's own winning a name clash and then the person's own, with two people's opened Skills of one name both kept as <their space>/<name>; a person's or an opened space that is missing, refusing or slow never fails a run and is left alone by runs for a minute; each person chooses only their own Bots (the default), every Bot, or the Skills they open one by one, stored in learning_skill_sharing and learning_open_skills, read by runs from an in-memory index that the saving replica refreshes at once and the others within 30 seconds, listed from the same Intelligence call delivery uses, with an unavailable line when Intelligence does not deliver them, and audited as learning.sharing_changed, learning.skill_opened and learning.skill_closed
… secret: the masking tests searched the whole serialised event for a secret's digits, so CI failed the 2 MiB form test when Date.now() read 1792433411150 and its "4111" matched the test card though the body was masked in full. Leak checks now search recordedText, the event with its timestamp, completedAt and durationMs set to 0 (and the page test's server port left out), which throws on any other clock-sized number so a new clock field cannot slip past; the tests that search events pin Date.now() to a reading holding their secrets' digits, so a check that read the clock again fails on every run
…ch click, edit, choice in a list, box ticked, form submit, Enter in a field and top-frame page change, in order, beside the narration and the outcome, so an agent can be trained to do the same clicks and enter the same data. Each step carries what an agent needs to find that element again (its accessible role and name, input type, a ranked list of stable selectors from test attributes and hand-made ids down to a short CSS path, never a generated id or a class name, its frame path inside an iframe, the page and its title, its box and the viewport), built in the page before masking so the masking covers it. No network call is recorded on its own: a step that saved something carries the one request it sent to save it (a PUT, PATCH, DELETE or GraphQL mutation, or a POST that carries what was typed or chosen or names a write; a configuration read through a POST, a query, telemetry and polling never), masked and cut to fit, the step held until its window closes or the next step starts and then sent with the request in place, steps in the order they came, on the debugger and the page-hook path alike.
…ecords each step you take (clicks, typing and the pages you open) with what it saves, and what you say; the Network row reads Capturing what each step saves (from inside the page when Chrome refused its debugger); the changelog says the same.
…hose path or operation names presence, a heartbeat, a ping, a keepalive, typing, a session refresh, telemetry, tracking or a log is never a step's save, nor is a read receipt the page posts when it shows a conversation (a PUT that marks a notification read stays the person's), so opening an assistant carries nothing; and page changes the page makes one after another, with no step of the person's between them and each within 2 s of the last (a sign-in's redirects, an app settling on its default view), are one navigate step to the final page, from the address the chain began at, while a reload or a step back is always its own.
…inside a button, link, menu item, tab, option, checkbox or another element a person acts on takes that element's role, name and selectors, keeping the box of what was clicked, and a click that meets none (a panel's empty space or text) names the clicked element by its own first 60 characters; every name read from text keeps a space between its pieces, so "Sign in" and "or create an account" no longer run together, and text a page hides from everyone is left out.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant