Policy and confirmation
How jawb classifies actions, the purchase gate, and how a human approves.
Before an Act command runs, jawbd classifies it and decides allow,
confirm, or deny. This is not opt-in — it applies to every agent
driving jawb, with no flag to switch it off.
| Class | Triggered by | Default |
|---|---|---|
upload | upload | confirm |
download | download, or navigation that starts a download | confirm when the file type is executable or unknown, otherwise allow |
submit | submit, or a click/Enter on a form's submit control | allow, unless the form has a password or payment field — then confirm |
purchase-like | submitting a form with payment fields, clicking a purchase-lexicon label (buy, pay, place order, subscribe...), or a commerce-adapter checkout | by default needs a human-issued approval — --yes never satisfies it (see the purchase gate) |
credential | fill on a password field or a card-number-shaped field | confirm |
confirm class
Everything except purchase-like follows this flow. On an interactive
terminal, jawb prompts you and, on your answer, retries with a
single-use confirm_token (expires after 60s). In --json mode or on a
non-interactive terminal, jawb exits 5 with E_CONFIRMATION_REQUIRED and
the token, so a calling agent can surface it or retry with
--confirm-token <t>. --yes pre-confirms this class without a prompt:
$ jawb upload --role textbox:Resume resume.txt
error: this upload action needs confirmation (E_CONFIRMATION_REQUIRED)
confirm token (upload): ct_c4wk43ztqc56vkq0cwzh
hint: re-run the same command with --confirm-token <token> (or --yes)
$ jawb upload --role textbox:Resume resume.txt --yes
upload [40] ok: 1 file(s) sent as bytes
The purchase gate
What jawbd does when it detects a purchase-like effect is the
purchase.gate setting (see Configuration). It can
be set globally and per profile; incognito sessions use the global value.
purchase.gate | Effect |
|---|---|
approve (default) | The action is held until a human approves it (below). |
deny | The action is refused with E_POLICY_DENIED (details.reason: "purchase_denied"). Nothing is held, and nobody is asked. |
allow | The action is not held, and the command's envelope carries the warning W_PURCHASE_UNGATED. Weaker than the default, so it takes effect only after a human approves it in the current daemon run (see weakening a safety setting). |
Detection modes
purchase.detection chooses how much jawb treats as a purchase.
standardapplies three layers: a label and form classifier; a marker on purchase-like form submissions that the network layer refuses unless a human approved the action; and an effect-level hold, during and for 3 s after an agent action, on state-changing requests to a checkout path or that carry a card number.strict(the default) is everything instandard, plus a payment-context hold. A tab enters payment context when its page has a payment field (autocomplete=cc-*, or a card-, CVC- or IBAN-like name), when it loads a frame from a known payment provider (Stripe, Adyen, Braintree, PayPal, Google Pay, Checkout.com, Square, Klarna), or when its top-level path names a purchase step such ascheckout. The context lasts 30 minutes after the last such signal, across navigations within the same site. While it lasts, every state-changing request from that tab (any non-GET/HEAD/OPTIONS request, a beacon or ping, or a page-initiated navigation with a query string) is held if an agent action is running, and otherwise refused. A refused request shows up as the warningW_PAYMENT_CONTEXT_REFUSEDon the tab's next command. An approved purchase arms the tab for that action and 60 s after it, so a multi-request checkout or a 3-D Secure step can finish.
The trade-off. strict holds more. On a checkout-adjacent page you
will be asked for approval on requests that are not purchases (a coupon
check, an address update), and a page that opens a WebSocket on such a
page may not work until a purchase is approved. If that costs more than
it protects, purchase.detection = "standard" is available, but it is a
weakening change that needs a human approval each daemon start.
What neither mode catches. These are known gaps, not edge cases to hope away:
- WebSocket handshakes are not blocked in either mode. Chrome's request interception does not see them.
- A page with no payment signals at all is not detected, for example a saved-card one-click button on an otherwise neutral page.
- A GET request that the server treats as an order, requests sent more than 3 s after an agent action when no payment context exists, and a purchase that a hostile page deliberately disguises.
The purchase gate is a safeguard for honest checkout flows. It is not a guarantee against a hostile page.
purchase-like class — human confirmation only
Under purchase.gate = "approve", purchase-like actions are held by the
daemon as a pending confirmation (pc_..., expires after
approval.timeout_s, 120 s by default) and return
E_CONFIRMATION_REQUIRED — but with no token, ever, to the agent that
triggered it:
$ jawb click --text "Place order"
error: this purchase-like action is held until a human approves it (E_CONFIRMATION_REQUIRED)
pending purchase-like confirmation: pc_348xpkg9ga5v (no token is issued to this process)
hint: a human must run `jawb confirm pc_348xpkg9ga5v` in their own terminal; then collect the result with --await-confirm pc_348xpkg9ga5v
$ jawb click --text "Place order" --yes
error: this purchase-like action is held until a human approves it (E_CONFIRMATION_REQUIRED)
pending purchase-like confirmation: pc_t0na2qwwbqmg (no token is issued to this process)
--yes and --json make no difference — the class is human-only in
every mode. A human approves it through one of the approval channels
below.
Approval channels
approval.channels lists the channels in order of preference. The default is
["desktop", "prompt", "terminal"].
- Desktop app (
desktop). The jawb desktop app (in development, not yet released) lists pending confirmations and approves or denies them. While the app is open it is treated as present, andjawbddoes not raise its own prompt unlesspromptis listed beforedesktop. - OS verification prompt (
prompt).jawbditself raises the operating system's user-verification dialog when a hold is created, with the text "Approve purchase on<host>:<amount>(jawb, profile<p>)". Approve with your PIN or biometrics and the purchase proceeds; cancel and the hold is denied. Today this exists on Windows only (Windows Hello). macOS and Linux have no verifier yet, so approvals there are recorded as "asserted" (see assurance labels). The prompt is skipped whenapproval.assuranceisasserted, when no verifier is available, and for 30 s after you deny a hold on the same tab (a quiet period). One prompt shows at a time; further holds queue. - Terminal (
terminal).jawb confirm. On the interactive terminal that is running the command, jawb shows the summary and asks you to typepurchase. From a separate terminal, at any time before the pending confirmation expires, runjawb confirm <pc_id>: it shows the summary, requires the typed wordpurchaseon an interactive TTY, and approves it. The agent that triggered the action re-collects the result withjawb trace --tab <t>or by re-running with--await-confirm <pc_id>.jawb confirmalso approves a weakening setting change (classconfig): it lists the changes and asks for the wordweakeninstead.
An approval from the terminal still goes through the OS verification where one is available: jawb letsjawbd's prompt come to the foreground, prints "Verify with Windows Hello on this computer", and waits until you answer it or the confirmation expires. It then prints the assurance label of the result, for exampleapproved pc_... (confirmed via tty-verified: the operating system verified you). The approval carries the confirmation'sdigest, so it only approves the request you were shown.jawb confirmneedsjawbdAPI level 12 or later and refuses an older daemon withE_VERSION_MISMATCH.
At the terminal prompts you can type deny instead to decline the action
(see Declining a held action). Anything else
leaves the action held until it expires.
When an approval is refused, jawb prints the reason and what it means:
verification_canceled (you canceled the OS prompt; still pending, try
again), verification_unavailable (no verifier could run and
approval.assurance is verified), verification_failed,
verification_in_progress (a prompt for it is already open),
digest_mismatch (the held request is not the one you were shown),
expired, denied, and superseded (a newer setting change replaced
it). Nothing is approved in any of these cases.
jawb confirm itself refuses to run non-interactively:
$ jawb confirm pc_from_above
error: `jawb confirm` only runs on an interactive terminal (no --json, no --yes, stdin must be a TTY) (E_CONFIRMATION_REQUIRED)
pending confirmation: pc_from_above (no token is issued to this process)
hint: run `jawb confirm` yourself in a terminal
This stops an agent that uses the jawb CLI as intended from approving
its own purchase: the CLI sends the separate confirmer credential only
after reading your typed confirmation on a real terminal.
Assurance labels
Every approval is recorded with confirmed_via, in the trace, in the
confirmation record and in the completed action's step. The label is
<channel>-<assurance>:
| Channel prefix | Meaning |
|---|---|
tty- | approved through jawb confirm (terminal) |
desktop- | approved in the desktop app |
prompt- | approved at the prompt jawbd raised itself |
| Assurance suffix | Meaning |
|---|---|
verified | the operating system reported a successful user verification (Windows Hello) to jawbd itself, so a program that merely read confirm.token could not have produced it |
asserted | the client claimed a human approved it and jawb could not verify that |
So prompt-verified and desktop-verified are the strong results;
tty-asserted is what you get on macOS and Linux today.
approval.assurance decides how the OS verification is used:
verified-if-available (default) verifies when a verifier exists and
otherwise records asserted; verified always verifies and refuses an
approval (reason: "verification_unavailable") when no verifier is
available; asserted never calls the verifier.
What an asserted approval does not stop. The daemon cannot tell a
person from a program running under your user account. The confirmer
credential is a file in the data directory (confirm.token), and any
process running as you, including an agent that ignores the CLI, can read
it and call the approval endpoint directly. Where an approval is only
asserted, treat the purchase prompt as a safeguard for well-behaved
agents, not as protection against a compromised or prompt-injected one.
A verified approval closes that gap
(ADR 0009).
One approval, one action
Each approval authorizes exactly one action. It is bound to the held
request (tab, action, target and arguments), and approving the same
pending confirmation again never runs the action a second time. An
approval must come before the pending confirmation expires
(approval.timeout_s); a request carrying the entry's digest that
does not match the held request is refused (reason: "digest_mismatch").
Weakening a safety setting
The settings that protect you (purchase.gate, purchase.detection,
approval.assurance) have a floor: their built-in default. A value in
the configuration file that is weaker than the default does not take
effect until a human approves it, through the same channels as a
purchase, and that approval lasts only until the daemon exits. The prompt
reads "Allow jawb to weaken a safety setting: <key> <from> → <to>".
Every daemon start with a weaker-than-default file asks again. This is
what stops an agent editing config.toml to switch the gate off. On a
terminal, jawb config set asks you to type weaken on the spot; an
agent's run, or --json, exits 5 with the pending ID, and you approve
it with jawb confirm <pc_id> and the word weaken. --yes never
approves it. See Configuration.
Listing pending confirmations
An approvals screen, such as the jawb desktop app, can list every
purchase-like confirmation still waiting for a human, without knowing
its pc_ ID, with GET /v1/confirmations (daemon API level 3 or
later; the level is in the x-jawb-api response header):
$ curl -s -H "Authorization: Bearer $(cat "$JAWB_DATA_DIR/daemon.token")" \
http://127.0.0.1:4180/v1/confirmations
{
"command": "confirmations",
"ok": true,
"result": {
"confirmations": [
{
"pending": "pc_v4kz827jhtw4",
"class": "purchase-like",
"state": "pending",
"tab": "t_qc78dj",
"action": "click",
"summary": {
"action": "click",
"target": { "id": 14, "label": "Place order", "role": "button" },
"url": "http://127.0.0.1:8767/checkout.html",
"form": "name=Jane Doe",
"amount": "$42.00"
},
"created_ms": 1791094203991,
"expires_in_ms": 119648,
"confirmed_via": null
}
]
},
...
}
Each item has the same fields as GET /v1/confirmations/<pc_id>. The
list also holds config confirmations (class config, with tab and
action null) raised when a safety setting is weakened, so a client must
not assume every item is purchase-like.
Only confirmations that are still pending appear, oldest first; once
one is approved, completed, denied or expired, it drops out. The list never
contains a token, and listing does not approve anything: approving
still needs the confirmer credential. Like every other /v1 call, it
needs the bearer token and is refused when the request carries an
Origin header. There is no CLI command for it; in a terminal,
jawb confirm <pc_id> remains the way to approve.
Declining a held action
A held action does not have to wait out its 120 s expiry. Type deny
at the jawb confirm <pc_id> prompt (or at the inline purchase prompt):
$ jawb confirm pc_c69grajbdcwm
jawb: t_t9f82t wants to perform a PURCHASE-LIKE action:
...
Type `purchase` to approve it, or `deny` to decline it: deny
denied pc_c69grajbdcwm; the held action was not performed
The action never runs. The agent that triggered it sees the refusal when it collects the result:
$ jawb --json click 14 --await-confirm pc_c69grajbdcwm
{"ok": false, "error": {"code": "E_POLICY_DENIED",
"message": "a human denied `pc_c69grajbdcwm`; the action was not performed",
"details": {"pending": "pc_c69grajbdcwm", "reason": "denied"}}, ...}
An approvals screen does the same with POST /v1/confirmations/<pc_id>/deny (daemon API level 4 or later). It needs
what approving needs: the bearer token, the confirmer credential in the
x-jawb-confirmer header, and no Origin header. After a deny:
GET /v1/confirmations/<pc_id>reports"state": "denied"for 10 minutes. This is a different state from"expired", which means that nobody answered.- The confirmation leaves
GET /v1/confirmations. - Denying it again returns the same
deniedrecord. - Approving it is refused with
E_POLICY_DENIEDandreason: "denied".
A confirmation that was already approved cannot be denied
(reason: "already_approved"), and an expired one cannot be either
(reason: "expired").
The limit described above applies to denying too. Any process running
as you that can read confirm.token can deny a held action, just as it
can approve one. A recorded deny is not proof that a person made the
decision.
deny
Returns E_POLICY_DENIED (exit 5). Nothing from the CLI can override it.
Every policy decision — class, decision, and whether/how it was
confirmed — is written to the trace (jawb trace).