Extensions
jawb extensions: the built-in app.jawb.* extensions, jawb extension and jawb tool call, consent, runs and their limits, app.jawb.uat and app.jawb.screens, and publishing on jawb Hub.
An extension is a package that adds tools to jawb. Its tools appear in
jawb tools beside the WebMCP tools a page declares, marked as coming from
the extension, and you run them with jawb tool call. Every step an
extension takes is an ordinary jawb command, so the same per-action policy,
confirmations and trace apply: a cheaper route is never a weaker one.
Not in a release yet: extensions need
jawbdat API level 18, and jawb's built-in extensions need level 20, with thejawbCLI after 0.1.1. jawb 0.1.1 and earlier have neither.
Version 0 runs one shape, workflow: a flat list of jawb steps that produces something, such as a report. There are two workflow extensions:
app.jawb.uatruns acceptance criteria against a site you develop and writes a report with a verdict per criterion.app.jawb.screensscreenshots pages of a site you develop at each viewport and colour scheme in one call.
Both are built into jawbd: their files are part of the jawbd binary
you installed, so jawb extension install app.jawb.uat needs no download,
and you still approve what it may do.
Extension ids are reverse-DNS names, like mobile app bundle ids: a
domain the publisher controls, reversed, then the name (com.example.tool),
or io.github.<user>.<name> without a domain. app.jawb.* is jawb's own
and is only used by the built-in extensions.
Tool names belong to their extension: the full name is the extension id
plus the tool's own name (app.jawb.uat.run, app.jawb.screens.capture),
so two extensions never clash. jawb tool call also takes any shorter
ending of the full name (uat.run, run) while only one installed tool
matches it; when two do, it exits 2 and lists the full names.
Commands
jawb extension has the short alias jawb ext.
| Command | What it does |
|---|---|
jawb extension list | Installed extensions: id, version, how far jawb vouches for it ([built-in], [listed], [unlisted], [unlisted, was listed] or [dev]), enabled or disabled (and why), how consent was given, and their tools. Also the built-in extensions not installed yet, and the catalogue index's serial and expiry. |
jawb extension info <id> | One extension: its manifest, what it may do (origins, sessions, secrets, files) and its tools. |
jawb extension install <id>[@<version>] | A built-in extension (app.jawb.*) installs from the copy inside jawbd, with no download. Any other id: downloads the signed catalogue index and the package from get.jawb.app, hands them to jawbd. Either way it asks for your consent. |
jawb extension install --file <pkg> | A third-party extension from a .tar.gz on disk. It installs as listed when jawb's signed index lists these exact bytes, else as unlisted, and the consent says which. See Third-party extensions. |
jawb extension install https://…/x.tar.gz#sha256=<hex> | The same from an https URL. The #sha256= pin is optional; with it, a download that does not match is refused before jawbd sees it. |
jawb extension load --dev <dir> | Loads an unpacked extension directory for development: unsigned, labelled dev everywhere, never updated from the catalogue, and still needs your consent. |
jawb extension remove <id> | Removes it and its files. Asks first; --yes skips the question. |
jawb extension refresh | Sends jawbd the newest signed index, so revocations reach it. Also jawb extension index. The daily background update check does this too when a daemon is running. |
jawb tools | Lists extension tools, plus the page's tools when there is a current tab (or --tab). |
jawb tool call <name> --input <json|@file> | Runs an extension tool. |
jawbd never downloads anything for extensions. The CLI fetches the files
and jawbd verifies them against the public keys compiled into it, so the
trust decision is made in one place. A changed byte fails verification.
With --json every command prints the usual
JSON envelope. An older jawbd is refused
with E_VERSION_MISMATCH (exit 3).
Installing
Installing or loading an extension is held for you, the same way a purchase is (Policy and confirmation). Nothing is written until you approve.
On an interactive terminal, jawb shows what the extension may do and
asks you to type install:
$ jawb extension install app.jawb.uat
jawb: installing an extension needs your consent (pc_8rwvaktb6c7p):
extension: app.jawb.uat 0.1.0 from jawb (built into jawbd)
purpose: Run acceptance criteria against a site you develop and write a verdict report with evidence
shape: workflow
origins: http://localhost:*, http://127.0.0.1:*
sessions: incognito, profile:uat
secrets: uat.username, uat.password
files: write:jawb-uat
network:
- reads from http://localhost:*, http://127.0.0.1:* with the sessions incognito, profile:uat
- sends nothing to other domains
- writes files under jawb-uat (relative to the working directory)
- fills the vault secrets uat.username, uat.password
An extension never approves anything; purchase-like actions stay held for you.
Type `install` to approve it, or `deny` to decline it:
Where the operating system can verify you (Windows Hello, Touch ID), it
asks too. Anywhere else (--json, a script, an agent) the command exits 5
with the pending id, and a person approves it later with
jawb confirm <pc_id>, which also asks for install. --yes never
approves an install. An update that asks for more (more origins, sessions,
secrets, files or a higher effect ceiling) asks again and lists what
widens.
Consent is "asserted" unless verified. jawb extension list shows
consent verified only when the operating system verified the person who
approved. Otherwise it says asserted: a program running as you could
have approved it.
Third-party extensions
jawb extension install --file <pkg> and jawb extension install <https-url> install an extension someone else publishes. They need jawbd
API level 28 and a current signed index from jawb, so jawbd can switch off
a revoked package. jawb does not publish that index yet, so until it
does these installs are refused (not_in_index); load --dev loads one you
are developing.
jawbd labels each install by what jawb knows about the bytes:
- listed: jawb's index lists these exact bytes. They passed jawb Hub's automated checks, which is not a review. The publisher shown is the listing's owner.
- unlisted: jawb has not checked it. The consent shows where it came from, and the publisher named in its manifest is shown as a claim, not a fact.
An unlisted package cannot reuse a listed id with other bytes, and no
third-party package may use jawb's name as its publisher or an app.jawb.*
id. Every install pins its sha256. Any new version of a listed or unlisted
extension asks you again, even one that asks for less, and so does the same
package from another source. Only workflows install this way for now.
Running a tool
jawb tool call uat.run --input '{"scenario": "checks/home.toml"}'
jawb tool call uat.run --input @input.json
jawb tool call looks the tool up, registers a run with jawbd
(which checks the input against the tool's schema), runs the steps, and
ends the run. Every step carries the run's id, so jawbd:
- refuses to open or act on an origin outside the extension's origins
(
outside_matches), including after a redirect; - refuses a session, a vault secret or an effect it was not granted
(
session_not_granted,secret_not_granted,exceeds_max_effect); - stops the run at its step and time limits (
run_limit); - lets a run call only page commands: never configuration, profiles,
secrets, cookies, storage or other extensions, and never an approval
(
outside_run_scope); - records the extension and the run on every trace step, labelled
attribution: "client-claim"(jawb traceshows it).
The steps run in the CLI, as an ordinary client of jawbd. These checks
bound what a step that names a run may do. They are not a sandbox.
Exit codes of jawb tool call:
| Exit | Meaning |
|---|---|
| 0 | The run finished. Its verdicts are in the output and the report, whatever they are. |
| 2 | Configuration error: bad input, an invalid scenario, an unknown or ungranted secret, an origin outside the extension's origins, an unusable output directory, an unknown tool, or a jawbd too old for extensions. |
| 3 | Harness drift: jawbd is not running or was lost, the browser bridge is off or missing, or the site under test does not answer. No verdicts are published. |
app.jawb.uat: acceptance tests with a report
app.jawb.uat runs the acceptance criteria of a scenario file against a site
on localhost or 127.0.0.1, and writes report.md and report.json
with one verdict per criterion, screenshots and trace ids.
Its grants: the origins http://localhost:* and http://127.0.0.1:*; an
incognito tab per criterion, or the uat profile; the vault secrets
uat.username and uat.password; files under ./jawb-uat in the
working directory; at most 500 steps and 900 seconds per run; and the
effect ceiling submit.
It is built into jawbd:
jawb extension install app.jawb.uat
Tools
app.jawb.uat.validate {scenario}checks a scenario and opens no page. It returns{ok, errors, unsupported_steps}and exits 0 when the scenario is valid, 2 when it is not.app.jawb.uat.run {scenario, profile?, incognito?, out_dir?, stop_on_first_fail?, step_timeout_ms?}runs it.scenario: a path to the scenario file (relative to the working directory), or the TOML inline.profile:"uat"runs every criterion in theuatprofile instead of incognito (needed forfill_secret).out_dir: where the report goes, inside./jawb-uat. The default isjawb-uat/<scenario id>. A path with.., or one that leads out of./jawb-uatthrough a link, is a configuration error.stop_on_first_fail: after a FAIL, the remaining criteria are reported UNCERTAIN, "not run".step_timeout_ms: how long eachexpect_*step waits (default 10000).
Scenario format
A scenario is a TOML file: a [scenario] table and one [[criteria]]
entry per acceptance criterion, each a flat list of steps. There are no
loops, conditionals or references to an earlier step's output. An unknown
key or step is an error, never ignored.
[scenario]
id = "issue-83-hero"
base_url = "http://localhost:3000"
issue = "4hum-ai/jawb#83" # optional, copied into the report
[[criteria]]
id = "AC1"
text = "The home page shows the product name in the hero"
steps = [
{ open = "/" },
{ expect_text = "jawb" },
{ screens = { viewports = ["mobile", "desktop"], schemes = ["light", "dark"] } },
]
[[criteria]]
id = "AC2"
text = "Signing in with a valid account reaches the dashboard"
steps = [
{ open = "/login" },
{ fill = { target = { role = "textbox", text = "Email" }, value = "uat@example.test" } },
{ fill_secret = { target = { css = "input[name=code]" }, ref = "uat.username" } },
{ click = { text = "Sign in" } },
{ expect_url = "**/dashboard", timeout_ms = 5000 },
]
Each criterion runs in its own new tab, which is closed at the end
whatever happens, so its first step must be open. A target is visible
text ("Sign in") or a table with text and/or role, or css, plus
nth when several elements match. Element ids are not accepted: they
belong to one observation.
| Step | What it runs |
|---|---|
open = "<path or URL>" | open (the first step opens the tab), then open --tab for later ones. Paths join base_url. |
click, hover, check, uncheck = target | The Act command of the same name. |
submit = true or target | submit. |
fill = { target, value } | fill. |
fill_secret = { target, ref } | fill --secret-from vault:<ref> in the run's profile. The value never reaches the CLI, the report or the trace. |
type = { target, text, delay_ms? } | type. |
select = { target, value or values, by? } | select (by = "label" to match labels). |
press = "Enter" or { key, target? } | press. |
scroll = "top", "bottom" or { by = [x, y] } | scroll. |
expect_text, expect_element, expect_url | wait --for text: / element: / url: with the step timeout. |
expect_no_text = "..." | read --format text --full, then a check that the text is absent. |
screens = { viewports, schemes } | For each viewport (mobile, desktop or WxH) and scheme (light, dark): viewport, emulate, screenshot. At most 8 combinations. |
screenshot = true or { full = true } | screenshot. |
capture = true or { full = true } | read --format md; the first 2000 characters go into the report, marked as untrusted page content. |
Any step may add timeout_ms. upload, download and media are known
but not run in version 0; uat.validate lists them under
unsupported_steps.
Verdicts
| Verdict | When |
|---|---|
| PASS | Every step of the criterion succeeded. |
| FAIL | An expect_* step timed out, or expect_no_text found its text. |
| UNCERTAIN | A step could not run: no element matched, several matched, a stale or not actionable element after one re-observe, a timeout outside an expect_* step, or any other error. |
| BLOCKED | jawb held an action for a person (E_CONFIRMATION_REQUIRED) or its policy refused it (E_POLICY_DENIED). The report names the pending id and its class. |
BLOCKED means the action was not approved during this run. The runner
never approves anything, so BLOCKED is not evidence that a person saw it.
A purchase-like click is always held, whatever the extension's ceiling.
Filling a password field is a credential action, above uat.run's
ceiling (submit), so a fill_secret into a password field is reported
BLOCKED with exceeds_max_effect; use it for other fields.
Harness drift is not a verdict: when jawbd, the browser bridge or the
site is missing, the run exits 3 and the report says harness_drift with
the step log only.
The report
uat.run writes report.md and report.json into the output directory,
next to its screenshots (<criterion>-<viewport>-<scheme>.png,
<criterion>-step<n>.png).
report.md has a header (scenario, issue, base URL, jawbd version and
API level, session, start and end times, the run's outcome and id), a
table with one row per criterion (id, text, verdict, reason, evidence
links, trace ids), and a step log per criterion.
report.json is the machine copy:
{
"v": 1,
"tool": "uat.run",
"extension": {"id": "app.jawb.uat", "version": "0.1.0", "dev": false},
"run_id": "r_dgrp3wd6xa4v",
"attribution": "client-claim",
"scenario": {"id": "smoke", "issue": null, "base_url": "http://127.0.0.1:4392", "source": "smoke.toml"},
"session": "incognito",
"jawbd": {"version": "0.1.1", "api_level": 18},
"started_at": "2026-10-05T23:19:17Z",
"ended_at": "2026-10-05T23:19:20Z",
"duration_ms": 3120,
"run": "completed",
"exit_code": 0,
"summary": {"PASS": 2, "FAIL": 1, "UNCERTAIN": 0, "BLOCKED": 0},
"criteria": [
{"id": "AC2", "text": "missing", "verdict": "FAIL",
"reason": "expect_text: condition `text:Nope` not met within 800 ms",
"evidence": [], "trace_ids": ["tr_01m475tjcngethv5sv", "tr_01m475tjct11netq93"],
"steps": [{"n": 2, "command": "expect_text", "detail": "\"Nope\"", "outcome": "fail",
"duration_ms": 813, "trace_ids": ["tr_01m475tjct11netq93"], "evidence": []}]}
]
}
Two runs of the same scenario against the same site give the same
report.json once the fields that name a moment or an id are set aside:
started_at, ended_at, every duration_ms, every trace id, run_id
and pending_confirmation. A CI job that should fail on anything but
PASS reads summary.
app.jawb.screens: screenshots across viewports and colour schemes
app.jawb.screens opens each page you name in a fresh incognito tab and takes
one screenshot per viewport and colour scheme. By default that is mobile
and desktop, light and dark: four PNGs per page.
jawb extension install app.jawb.screens
jawb tool call screens.capture --input '{"base_url": "http://localhost:3000", "pages": ["/", "/pricing"]}'
The files land in ./jawb-screens/screens/: home-mobile-light.png,
home-mobile-dark.png, home-desktop-light.png, home-desktop-dark.png,
the same four for pricing, and report.md and report.json. The
output's pages lists each page with its verdict and its files, as paths
relative to the working directory.
Its grants are:
- the origins
http://localhost:*andhttp://127.0.0.1:*; - an incognito tab per page, with no profile, so no cookies or sign-ins;
- files under
./jawb-screens; - at most 500 steps and 900 seconds per run;
- the effect ceiling
act. It changes the viewport and the colour-scheme emulation and takes screenshots. It never clicks, fills or submits.
app.jawb.screens.capture {base_url, pages?, viewports?, schemes?, name?, out_dir?, step_timeout_ms?}:
base_url: the site's address.pages: paths or URLs, 1 to 20 (default["/"]). A page outside the origins above is a configuration error (exit 2), and nothing is opened.viewports:mobile,desktoporWxHsuch as390x844(default["mobile", "desktop"]).schemes:lightand/ordark(default both). There are at most 8 viewport and scheme combinations.name: names the run and its folder (defaultscreens).out_dir: where the files go, inside./jawb-screens.
A page is PASS when all its screenshots were written. A page that does not load is UNCERTAIN, with the reason. Exit codes are the ones in Running a tool.
Code extensions
A code extension runs its own JavaScript on Node.js, for what a list of
jawb steps cannot do: parse a file, call a local tool, compare results. Its
tools use the exec runner, and the package carries a bundle:
[runtime]
node = ">=22"
entry = "dist/extension.mjs"
[[tools]]
name = "summarise"
description = "Open a page and return its first line"
effect = "act"
max_effect = "act"
runner = "exec"
input_schema = "schemas/summarise.input.json"
output_schema = "schemas/summarise.output.json"
The entry module exports activate(ctx) and registers each tool the
manifest declares:
export function activate(ctx) {
ctx.registerTool("summarise", async (input, run) => {
const opened = await run.jawb.open(input.url, { incognito: true });
const t = opened.result.tab;
const read = await run.jawb.tab(t).read({ format: "text" });
await run.jawb.tab(t).close();
return { first_line: read.result.content.split("\n")[0] };
});
}
run.jawb.* is jawb's API for the call: each method is one request to
jawbd, made by the CLI for this run, so the run's limits, origins, policy,
confirmations and trace apply to it. A call after the tool has returned is
refused (no_run). The tool's return value is checked against its
output_schema before the call ends (invalid_output).
Code runs with your full access. It is a Node.js process with your user
account's rights, not a sandbox: it can read your files and reach the
network on its own. The manifest's grants ([matches],
browser_sessions, [limits], max_effect) bind what it asks jawb to do
through run.jawb.*, and jawb records and gates those calls, but for the
code itself they are advisory: with your account's access it could read
jawb's own files or drive a browser without jawb. Install code only from a
publisher you trust. So installing a code extension says so ("runs code on
this computer with full access to your user account"), and only an approval
your operating system verifies (Windows Hello, Touch ID or your password)
installs it; a typed install alone is refused
(code_install_needs_verification). Any change to its files asks again.
jawb extension list marks it [runs code: full user access].
How the CLI runs it:
- Node.js 22 or later must be on your
PATH(or setJAWB_NODE); otherwise the call exits 2 withruntime_missing. - Checked first. Before it starts the code, the CLI checks every file
against the digests you approved, and refuses with
package_modifiedwhen one changed. - A clean environment. The process gets only
PATH(and, on Windows,SystemRoot,TEMPandTMP): noJAWB_*variable and no daemon token. It runs in its own working directory under%LOCALAPPDATA%\jawb-exthost(Windows) or~/.cache/jawb-exthost, whichjawb extension removedeletes. - One process per call for
jawb tool call;jawb mcpkeeps one per extension for the session. A call that passes[limits] max_run_secondsis stopped, and the process is killed together with everything it started.[limits] memory_mbcaps its heap. console.loggoes to the CLI's log, never into the tool's output or the trace.
Not yet: hooks (ctx.registerHook throws until #198), and activation
conditions beyond "a tool was called". Code extensions need jawbd at API
level 35 and a jawb CLI after 0.1.1.
Publish your extension on jawb Hub
Community extensions are listed on
jawb Hub, a public GitHub
repository, and shown on jawb.app/extensions.
Listed means the exact bytes passed jawb Hub's automated checks. It is
not a review, a signature or a warranty: jawb does not vouch for the code.
The listing terms
apply. The author guides live there too: a tutorial,
Build and list your first extension,
the manifest reference,
and a template to copy, examples/site-screens.
- Host your extension yourself, as a release
.tar.gz(a GitHub release asset, for example) or an npm package. Your code stays in your repository, under your licence. - Prove you own its id.
- For
io.github.<you>.<name>, your GitHub account is the proof. - For a domain id (
com.example.hellonamesexample.com), publishgithub:<your login>, either inhttps://example.com/.well-known/jawb-hub.txtor as a DNS TXT record_jawb-hub.example.com.
- For
- Open a pull request that adds one file,
listings/<id>.toml, with the id, its owners, a homepage, and for each version its source and the sha256 of the exact file. - The checks run on the pull request:
- fetch the package from its source and compare its sha256;
- the package rules;
- the manifest, as
jawbdchecks it; - hidden or bidi characters in anything a model reads;
- the never-list;
- ownership of the id.
A maintainer merges a green pull request without reviewing your code, and the listing appears on jawb.app. A listed version never changes: publish a new version instead.
Once a listing is merged, jawb extension install <id> installs it from
jawb's signed index, labelled listed. install <url> and install --file
take the same bytes from anywhere (see
Third-party extensions).
What comes later
These parts of the extension design are planned, not built.
- More shapes. Site tool packs (a script in the matching tab, in its
own isolated world) and app connectors or API tools (
jawbdadds a stored secret at the network layer; the extension never sees it). A stored secret will carry the domains it may be sent to, so an extension cannot move it elsewhere. Two more shapes are reserved: policy packs, which can only make jawb's decisions stricter, and readers, which turn jawb's own view of a page into structured data. - More third-party sources. Installing from npm and from a git repository, besides a file and a URL (built, waiting for jawb's signed index).
- jawb's signed index. jawb Hub's listings will reach
jawbdin a signed index with a short expiry, which also carries a revocation list, by version or by the package's sha256. A revoked package is switched off and its data kept. An out-of-date index stops new installs but never blocks browsing.jawbdalready enforces all of this; jawb does not publish the index yet. - The desktop app. Discovering, installing and removing extensions in the jawb desktop app, from the same signed catalogue the CLI uses.
- Hooks. Code extensions subscribing to what happens in jawb (a page
loaded, an action done, a tool called), from the same events
jawb trace --follow --jsonshows. - An isolated host. A WebAssembly Component host (Wasmtime) where extension code gets only what its manifest grants, with per-plugin quotas, for extensions that do not need full access.
- Free and paid. Free and paid extensions are planned. jawb itself is free for personal developers; business and team use will need a paid licence.
What jawb will never list
- LinkedIn data extraction or outreach tools. LinkedIn's terms name browser add-ons.
- Coupon auto-appliers at checkout.
- CAPTCHA solvers, and anything that evades rate limits or bot defences.
- Generators that pass a second factor unattended.
- AI sidebar clones: your agent's own model does that job.
The catalogue also refuses rules that loosen anything, hooks that change how the browser launches or what every page sees, native binaries, and extension code inside the jawb desktop app.
See Roadmap and status for what is built and what is planned.