# Obstacles and captchas

What Kaabist does about the dialog, consent banner, tour or toast in the way of your step, how it learns a site, and what it does about a captcha.

An obstacle is anything painted over the element a step wants: a cookie banner, an announcement dialog, a product tour, a toast. Kaabist detects it, closes it the way a person would, checks that it is gone, and tells you what it did, so a program does not need a step for every overlay a site might show.

## When to use it

The obstacle layer is always on in the browser tiers. You configure it when a site needs something different from the defaults: an overlay that is part of the page and must be left alone (`options.obstacles.ignore`), a modal that *is* the page and must never be painted over (`options.obstacles.allow_hide: false`), or a captcha policy other than parking (`policies.captcha`).

Read this page when a result carries an `obstacle_dismissed` warning, when a job parks with the reason `obstacle` or `captcha`, or when the review queue offers a `dismiss` proposal.

## When it runs

Before every element action (`click`, `fill`, `hover` and the rest), and whenever a step fails because its target is covered, Kaabist looks at what is in the way. A step that cannot click a button because something sits on top of it is the most common way an otherwise correct program fails, so this is treated as a normal situation rather than an error.

## What counts as an obstacle

Four detectors run inside the page, and anything found by any of them is a candidate:

1. **Semantics**: `role="dialog"`, `role="alertdialog"`, `aria-modal="true"`, `<dialog open>`.
2. **Geometry**: a fixed, sticky or absolutely positioned element that covers at least **40 %** of the viewport, or that sits over the exact point your step was aiming at, and that is either stacked above `z-index: 100` or accompanied by a `<body>` that has stopped scrolling.
3. **Markers**: the frameworks that name themselves. Consent: OneTrust, Cookiebot, Didomi, Usercentrics, Osano, Quantcast, TrustArc, Sourcepoint, cookieconsent, Klaro, Axeptio, CookieYes, Complianz, Borlabs, and the common generic ids and classes (`#cookie-banner`, `.cookie-consent`, …). Tours: driver.js, Shepherd, intro.js, react-joyride, Hopscotch, reactour. Toasts: `[role=alert]`, `[role=status]`, react-toastify, Sonner, MUI Snackbar, Notyf.
4. **The hit test**: whatever the browser says is actually painted at the point of your target.

Each obstacle comes back with a **kind**, how much of the viewport it covers, whether it is over your target, and every button inside it that might make it go away:

| kind | what it is |
|---|---|
| `dialog` | a modal dialog |
| `consent` | a cookie or privacy banner, or a consent wall |
| `tour` | an onboarding or product tour |
| `toast` | a notification stack |
| `overlay` | a covering layer that is none of the above |
| `unknown` | something covers the target and nothing more can be said about it |
| `busy` | a loading overlay: a spinner, a block-UI layer, an `aria-busy` element |

A `busy` overlay is **waited for**, never dismissed: the step waits until it goes away by itself, within the step's timeout, and the wait does not count toward the guards below.

## Which button, and in what order

A button's label is read against the **dismiss dictionary**: nearly three hundred phrases in sixteen languages (English, Turkish, German, French, Spanish, Italian, Portuguese, Dutch, Polish, Russian, Arabic, Japanese, Chinese, Korean, Swedish, Czech) plus the symbols close icons use (`×`, `✕`, `✖`, `✗`, `⨯`, `X`). The dictionary is data, not code: it grows when somebody takes over a job and clicks their way out of an overlay Kaabist did not recognise, site by site, and a phrase that three different sites have taught becomes part of everyone's dictionary.

Labels are read from the button's text, its `aria-label` and its `title`, and each is sorted into one of six kinds. The order is what a person would do:

| kind | example | what it costs you |
|---|---|---|
| `close` | Close, Kapat, Schließen, 閉じる | nothing |
| `reject` | Reject all, Tümünü reddet, Alle ablehnen | refuses tracking on your behalf |
| `later` | Not now, Daha sonra, Später | nothing |
| `skip` | Skip, Atla, Überspringen | nothing |
| `accept` | Accept all cookies, Tümünü kabul et | **agrees** on your behalf |
| `next` | Next, Sonraki, 次へ | nothing, and it is never clicked |

An unlabelled control is scored by where it sits: a small button in the obstacle's top-right corner is a close icon on nearly every site, and one in the middle of a dialog is a call to action.

## The resolution order

Rungs are tried in order, and **each one is verified before the next is tried**: the overlay has to be gone, or your target has to be reachable again, within 1.5 seconds. "The click did not throw" is not success.

1. **A recipe you approved.** If somebody in your organisation approved a `dismiss` proposal for this site with **remember for this site**, that recipe runs first. This is the fast path: one click, no guessing.
2. **ARIA.** A control inside the obstacle whose `aria-label` says it closes: the page telling Kaabist in its own words.
3. **The dictionary.** The best-scoring `close`, `reject`, `later` or `skip` control. **`accept` is used only for a consent banner that offers nothing else**, and when it happens the warning carries the detail `accepted_consent`, so you can see that a consent decision was made for you.
4. **`Escape`**, on the document.
5. **A backdrop click**, at a point inside the dimmer and outside the dialog.
6. **Hiding**, with an injected `<style>`; see the gating rule below.
7. **Parking.** The job stops as `needs_attention` with the reason `obstacle`, a screenshot, and one proposal per button nobody tried, so approving one makes the next run automatic.

After a successful dismissal the page is looked at again, because obstacles come in sequences: a consent wall in front of a tour that only starts once consent is given. A second overlay is dealt with when it covers your target or at least 40 % of the viewport.

### What it reports

A step that dismissed something carries one warning, naming the rung that finished the job:

```json
{ "code": "obstacle_dismissed", "step": "steps[1]", "detail": "dictionary" }
```

`detail` is the rung (`recipe`, `aria`, `dictionary`, `escape`, `backdrop`, `hide`), or `accepted_consent` when a consent banner had to be accepted. The step's record in `GET /v1/jobs/{id}/steps` has an `obstacle` object with the whole story: what was detected, its `kind`, whether it was resolved, `how`, the label of the control that was clicked (`control_text`), the locators of the overlay's root, and how many rungs were tried.

### How Kaabist learns a site

A dismissal by ARIA, the dictionary, `Escape` or hiding files a `dismiss` proposal: the overlay (`payload.obstacle`), what closed it (`payload.action`, such as `{ "click": [ … ] }` or `{ "press": "Escape" }`), and the `hostname`. Approving it with `remember: "site"` turns it into a recipe on your organisation's site profile, and the next job on that hostname closes the overlay at rung 1. A backdrop click is never proposed: it is a point on the screen, and the same overlay on a wider window has a different one. The same proposal is filed at most once a day per site and action, so a busy schedule does not fill your review queue.

## When Kaabist will hide something, and when it will not

Hiding does not dismiss an overlay, it paints over it, so it is last, and it is gated by kind:

- Always allowed for `overlay`, `toast`, `tour` and `unknown`. None of those carries a decision.
- For `dialog` and `consent`, allowed only while `options.obstacles.allow_hide` is on. It is on by default. Set it to `false` for a page whose modal **is** the page (an age gate, a paywall, a terms dialog), and the job will park and ask a person instead of continuing behind it.

You can also tell Kaabist to leave something alone:

```json
{ "options": { "obstacles": { "allow_hide": false, "ignore": ["#support-bubble"] } } }
```

An element matching an `ignore` selector (at most 20) is never reported as an obstacle and never clicked.

## Guards

A site that re-renders its banner after every dismissal would otherwise spend a whole job clicking the same button and succeeding every time. Three limits stop that, and passing any of them parks the job as `needs_attention: obstacle` with the detail `loop_guard`:

- three clicks on one obstacle,
- three obstacles in one step,
- ten dismissals in one job.

## Captchas

`policies.captcha` decides what a captcha means for your program:

| policy | what happens |
|---|---|
| `park` (default) | the job parks as `needs_attention` with the reason `captcha`, so a person can finish it |
| `fail` | the job fails at once with `blocked` |
| `solve` | a captcha provider is asked for a token, at **two credits per solve** |

Kaabist recognises reCAPTCHA (v2, v3 and invisible), hCaptcha, Turnstile, GeeTest and FunCaptcha/Arkose by their widget attributes and script URLs, never by prose, so a page that *writes about* reCAPTCHA is not treated as one. Under `solve`, the credits are charged before the provider is asked, the token is written into the field the vendor uses, the page's own callback is invoked, and the failed step is run again once.

Everything that can go wrong with the platform's solver **parks**: no provider configured, not enough credits, a provider that timed out (two minutes), or a second challenge on the same step after a solved one. A captcha is the one obstacle a person can always get past, so stopping to ask is the right answer. If you [bring your own solver](https://docs.kaab.ist/guides/byo-providers) and it refuses your key or runs out of balance, the job fails with `byo_captcha_auth_failed` or `byo_captcha_balance` instead, because that is something only you can fix, and its solves are not billed by Kaabist.

## Worked example

Three situations show the layer at work. Each run needs a browser tier.

**A consent wall in front of a dialog.** A product page at `https://shop.example.com/p/1` shows a full-screen consent wall (`#cookie-banner`, buttons *Accept all* and *Reject*) and, 300 ms after load, an announcement dialog behind it. A program that clicks the price:

```json
{
  "kaab": "1",
  "options": { "max_tier": "browser" },
  "steps": [
    { "goto": "https://shop.example.com/p/1" },
    { "sleep": 600 },
    { "click": ".price" }
  ]
}
```

The `click` finds `.price` covered. The layer detects the wall as `consent` by its marker and clicks *Reject*: the dictionary prefers `reject` to `accept`, so no consent was given on your behalf and there is no `accepted_consent` detail. It looks again, finds the dialog now in front of the price, and closes it with its *Anladım* ("Got it") button. The job succeeds with one warning, `{ "code": "obstacle_dismissed", "step": "steps[2]", "detail": "dictionary" }`. The step record's `obstacle` names the last thing it dealt with, `kind: "dialog"`, `resolved: true`, `how: "dictionary"`, two attempts, and the review queue has two `dismiss` proposals, one for the wall and one for the dialog, each with `source: "heuristic"` and a confidence of 0.85.

Approve both with `remember: "site"` and the next job on that hostname closes each overlay by recipe, with `detail: "recipe"`, and files nothing new.

**A product tour.** The app at `https://app.example.com` opens a driver.js-style three-step tour over its product list, with *Next* and *Skip*. A step that clicks `#total` finds it covered; the tour is detected by its marker as `tour`, and the layer clicks *Skip*, never *Next*. The step record shows `kind: "tour"` and `control_text: "Skip"`.

**When it parks.** A page can also show a dialog whose only way out is an unlabelled glyph. With `"options": { "obstacles": { "allow_hide": false } }`, no rung can close it and hiding is forbidden, so the job parks with `status: "needs_attention"` and `reason: "obstacle"`, a screenshot in `attention.screenshot`, and a proposal per control nobody tried. You can apply one with `POST /v1/jobs/{id}/resume`, or [take over](https://docs.kaab.ist/panel/takeover) the job and close it yourself; what you click becomes a `dismiss` proposal for the next run.

## Pitfalls

- **Look for `accepted_consent`.** If a site's banner offers only *Accept*, Kaabist accepts it to continue, and says so. If agreeing is not acceptable for your use, treat a result with that detail as a failure in your own code, or deal with the banner in an explicit step of your program.
- **`allow_hide` defaults to on.** A paywall or an age gate is a `dialog` too; turn hiding off for programs where the modal's content matters.
- **Ignore furniture.** A chat bubble or a sticky header that sits over a target will be dismissed every run unless you list it in `ignore`.
- **A loop guard means the site fights back.** Three dismissals of the same overlay usually mean the site re-renders it on purpose. Approve a recipe, or take over once and let the recording teach the site.
- **The http tier has no overlays to close.** It reads the HTML the server sent; there is nothing painted over it. Obstacles matter only once a job runs in a browser.

## Reference

- [`options.obstacles` and other options](https://docs.kaab.ist/reference/dsl/options) and [`policies.captcha`](https://docs.kaab.ist/reference/dsl/policies)
- [Steps of a job](https://docs.kaab.ist/reference/api/jobs/list-job-steps), [resume a job](https://docs.kaab.ist/reference/api/jobs/resume-job), [take over a job](https://docs.kaab.ist/reference/api/jobs/takeover-job)
- [Approve a proposal](https://docs.kaab.ist/reference/api/proposals/approve-proposal)
- [Attention reasons](https://docs.kaab.ist/reference/codes#attention-reasons) and [warning codes](https://docs.kaab.ist/reference/codes#warnings)
- [How healing works](https://docs.kaab.ist/concepts/self-healing): the review queue that `dismiss` proposals share with relocations
