Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/DEMO-STRIPE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ The script is a plain MCP client: it uses only the server's tools, exactly as an
5. `type_text` the email, name, and ZIP. These are not secrets; they round-trip visibly.
6. `fill_secret` three times: card number, expiry, CVC.
7. `snapshot` β€” all three card fields show `[REDACTED:secret-filled-field]`.
8. `click` Pay; `snapshot` until the receipt page replaces the form.
8. `click` Pay; `snapshot` until the receipt page replaces the form. The snapshot `text` shows the confirmation message.

The result: a completed purchase in the Stripe test dashboard, and a transcript in which the card number never appears.

Expand Down
2 changes: 1 addition & 1 deletion docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Turnkey's export flow fits this exactly. `exportSecret` re-encrypts the secret t
4. `SecretsClient.exportSecret(ref)` β€” plaintext lands in broker memory.
5. Register the value with the `RedactionRegistry` **before** injection.
6. For each target: observe and authorize again, require the same destination (`isSameDestination`), register the target, then inject over CDP `Input.insertText`. Never through `Runtime.evaluate`.
7. Drop the value. Return outcome only: `{ filled: true }`.
7. Read each field back over CDP and compare it to the value in the broker. Drop the value. Return the outcome and non-secret facts per field only: `{ filled: true, confirmed, fields: [{ length, matches, valid }] }`. Never the value or any part of it.

## Destination binding

Expand Down
12 changes: 6 additions & 6 deletions docs/HOSTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,11 @@ The broker caches each JWKS for 10 minutes and refetches it, at most every 30 se

**Scopes.** Each tool needs one scope:

| Scope | Tools |
| ------------ | ------------------------------------------------------------------------------------------------------ |
| `sbm:browse` | `open_browser`, `close_browser`, `navigate`, `snapshot`, `click`, `type_text`, `list_network_requests` |
| `sbm:fill` | `fill_secret`, `await_fill` |
| `sbm:refs` | `list_secret_refs` |
| Scope | Tools |
| ------------ | ----------------------------------------------------------------------------- |
| `sbm:browse` | `open_browser`, `close_browser`, `navigate`, `snapshot`, `click`, `type_text` |
| `sbm:fill` | `fill_secret`, `await_fill` |
| `sbm:refs` | `list_secret_refs` |

The scopes apply the same way to both protocol versions. Opening a session needs at least one of these scopes; without one, the broker answers `403 insufficient_scope` with `scope="sbm:browse"`. Listing tools needs nothing more. An agent that fills secrets needs all three. The broker checks scopes on every request against that request's token, so a client that steps up its token gets the new scopes in the same session.

Expand Down Expand Up @@ -211,7 +211,7 @@ The broker picks the era for each request with the MCP TypeScript SDK's own clas

## Browser handles

A browser handle is a browser that does not belong to one connection. `open_browser` returns a `browser_id`. Every browser tool (`navigate`, `snapshot`, `click`, `type_text`, `fill_secret`, `await_fill`, `list_network_requests`) takes an optional `browser_id` and then acts in that browser. `close_browser` with the `browser_id` closes it.
A browser handle is a browser that does not belong to one connection. `open_browser` returns a `browser_id`. Every browser tool (`navigate`, `snapshot`, `click`, `type_text`, `fill_secret`, `await_fill`) takes an optional `browser_id` and then acts in that browser. `close_browser` with the `browser_id` closes it.

- Use handles with a 2026-07-28 client: it has no session, so a browser tool without a `browser_id` fails. Use them also with a 2025 client that opens a new session for each tool call (the Arcade gateway). A browser tied to such a session is gone before the next call.
- On a 2025 session, a call without `browser_id` uses the session's own browser, as before. The session's browser and its handles are separate browsers.
Expand Down
10 changes: 7 additions & 3 deletions site/docs.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,13 @@ <h1>The safe fill workflow</h1>
For JSON payloads, pass its declared field keys in one call.
</li>
<li>
<strong>Verify and submit.</strong> A follow-up snapshot shows a
structural redaction marker instead of a value. Click the submit
control, then snapshot until the page changes.
<strong>Verify and submit.</strong> The
<code>fill_secret</code> result says if each field kept its value
(<code>confirmed</code>, and <code>matches</code>,
<code>length</code> and <code>valid</code> per field), never the value
itself. A follow-up snapshot shows a structural redaction marker
instead of a value. Click the submit control, then snapshot until the
page changes.
</li>
</ol>
<h2>Destination bindings</h2>
Expand Down
2 changes: 1 addition & 1 deletion site/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The agent drives a browser but never reads secret material. Treat every secret a
2. **Navigate and inspect.** Use `navigate`, then `snapshot`. Snapshot element IDs become invalid after navigation, so take a fresh snapshot whenever the page changes.
3. **Fill visible values normally.** Use `type_text` only for non-secret values such as email or ZIP code.
4. **Fill credentials by reference.** Use `fill_secret` with the secret ID and current element ID. For JSON payloads, pass declared field keys in one call.
5. **Verify and submit.** A follow-up snapshot shows a structural redaction marker instead of a value. Click submit, then snapshot until the page changes.
5. **Verify and submit.** The `fill_secret` result says if each field kept its value (`confirmed`, and `matches`, `length` and `valid` per field), never the value itself. A follow-up snapshot shows a structural redaction marker instead of a value. Click submit, then snapshot until the page changes.

## Destination bindings

Expand Down
15 changes: 5 additions & 10 deletions site/reference.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ <h1>MCP operations</h1>
<td>None</td>
<td>
Returns interactive elements with IDs for subsequent
click/type/fill operations. Filled values are redacted.
click/type/fill operations, and the page's headings and alert or
status text. Filled values are redacted.
</td>
</tr>
<tr>
Expand All @@ -76,8 +77,9 @@ <h1>MCP operations</h1>
<code>fields</code>
</td>
<td>
Checks the binding, requests export, then injects via CDP. May
return <code>pending_approval</code>.
Checks the binding, requests export, injects via CDP, then reports
per field if the page kept the value (never the value). May return
<code>pending_approval</code>.
</td>
</tr>
<tr>
Expand All @@ -91,13 +93,6 @@ <h1>MCP operations</h1>
pending fill; it can remain pending.
</td>
</tr>
<tr>
<td><code>list_network_requests</code></td>
<td>None</td>
<td>
Reserved scaffold; currently returns <code>not_implemented</code>.
</td>
</tr>
</tbody>
</table>
<h2>Fill shapes</h2>
Expand Down
19 changes: 9 additions & 10 deletions site/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,15 @@ html: https://tkhq.github.io/secure-browser-mcp/reference.html

The tool surface is intentionally small. All results pass through the server's redaction layer.

| Operation | Input | Result / behavior |
| ----------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `list_secret_refs` | None | Opaque IDs, names, and destination bindings. Never returns secret values. |
| `navigate` | `url` | Navigates the broker-owned browser and returns its final URL and title. |
| `snapshot` | None | Returns interactive element IDs. Filled values are redacted. |
| `click` | `element_uid` | Clicks an element from the most recent snapshot. |
| `type_text` | `element_uid`, `text` | Types visible, non-secret text. Never use it for a credential. |
| `fill_secret` | `secret_id` plus `element_uid`, or `fields` | Checks the binding, requests export, then injects via CDP. May return `pending_approval`. |
| `await_fill` | `fill_id`, optional `timeout_seconds` (1–120) | Waits for approval, revalidates the target, and completes a pending fill; it can remain pending. |
| `list_network_requests` | None | Reserved scaffold; currently returns `not_implemented`. |
| Operation | Input | Result / behavior |
| ------------------ | --------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `list_secret_refs` | None | Opaque IDs, names, and destination bindings. Never returns secret values. |
| `navigate` | `url` | Navigates the broker-owned browser and returns its final URL and title. |
| `snapshot` | None | Returns interactive element IDs, headings, and alert text. Filled values are redacted. |
| `click` | `element_uid` | Clicks an element from the most recent snapshot. |
| `type_text` | `element_uid`, `text` | Types visible, non-secret text. Never use it for a credential. |
| `fill_secret` | `secret_id` plus `element_uid`, or `fields` | Injects via CDP if the binding holds; says if each field kept it. May return `pending_approval`. |
| `await_fill` | `fill_id`, optional `timeout_seconds` (1–120) | Waits for approval, revalidates the target, and completes a pending fill; it can remain pending. |

## Fill shapes

Expand Down
4 changes: 2 additions & 2 deletions skills/secure-browser/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ Snapshots and fills target the main frame only. Cross-origin iframe inputs, incl
3. `snapshot` β€” returns interactive elements with `uid`s. Element uids are invalidated by navigation; re-snapshot after every navigation.
4. Fill non-secret fields (email, name, ZIP) with `type_text`. These are visible in your transcript, which is fine β€” they are not secrets.
5. Fill secret fields with `fill_secret`. Single-value secrets take one `element_uid`. A secret whose binding lists `sbm:fields` holds a JSON payload (e.g. a card's number/expiry/cvc): pass `fields: [{key, element_uid}, …]` to fill all of them in one call β€” one export, one approval.
6. `snapshot` to verify: the filled field's value reads `[REDACTED:secret-filled-field]`.
7. `click` the submit button, then `snapshot` (repeat with short waits) until the page moves on.
6. Check the `fill_secret` result: `confirmed: true` means every field holds its value. If a field has `matches: false`, `length: 0`, or `valid: false`, the page dropped or rejected the value; report it and do not submit. A later `snapshot` shows filled fields as `[REDACTED:secret-filled-field]`.
7. `click` the submit button, then `snapshot` (repeat with short waits) until the page moves on. Read the snapshot's `text` (headings and alert or status messages) to confirm success or see the error.

## Browser handles

Expand Down
50 changes: 41 additions & 9 deletions skills/secure-browser/references/tools.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Tool reference

Nine working tools and one registered, unavailable stub. All results are redacted server-side before you see them.
Nine tools. All results are redacted server-side before you see them.

Every browser tool (navigate, snapshot, click, type_text, fill_secret, await_fill, list_network_requests) also takes an optional `browser_id` from `open_browser`. With it, the tool acts in that browser. Without it, the tool uses the connection's own browser. A connection with no browser of its own (an MCP 2026-07-28 client) must pass `browser_id`.
Every browser tool (navigate, snapshot, click, type_text, fill_secret, await_fill) also takes an optional `browser_id` from `open_browser`. With it, the tool acts in that browser. Without it, the tool uses the connection's own browser. A connection with no browser of its own (an MCP 2026-07-28 client) must pass `browser_id`.

## open_browser

Expand Down Expand Up @@ -42,7 +42,11 @@ No tool ever returns a secret's value.

## snapshot

No arguments. Returns `{ url, title, elements: [...] }` where each element has:
No arguments. Returns `{ url, title, text: { headings, alerts }, elements: [...] }`.

`text` is a short summary of what the page says, for reading results and errors: `headings` holds up to 20 visible headings, and `alerts` up to 10 visible alert, status, and live regions (`role=alert`, `role=status`, `aria-live`, `<output>`). Each entry is at most 200 characters. It is not the full page text. A secret the page shows as text is replaced with `[REDACTED:<secretId>]`.

Each element has:

- `uid` β€” targeting handle for click/type_text/fill_secret; valid until the next navigation
- `tag`, `type`, `name`, `label`, `text`, `value` as available
Expand All @@ -59,16 +63,44 @@ Field values are redacted: `[REDACTED:secret-filled-field]` after a fill, `[MASK

## fill_secret

`{ secret_id: string, element_uid?: string, fields?: [{key, element_uid}] }` β†’ outcome only (no value). Steps performed broker-side: resolve ref β†’ resolve elements β†’ enforce binding (origin, URL pattern, selector) β†’ export from Turnkey β†’ inject via CDP β†’ tag fields for redaction. Throws a binding-violation error if the live page or an element doesn't match the secret's binding; do not retry elsewhere.
`{ secret_id: string, element_uid?: string, fields?: [{key, element_uid}] }` β†’ outcome and read-back facts (never the value). Steps performed broker-side: resolve ref β†’ resolve elements β†’ enforce binding (origin, URL pattern, selector) β†’ export from Turnkey β†’ inject via CDP β†’ tag fields for redaction. Throws a binding-violation error if the live page or an element doesn't match the secret's binding; do not retry elsewhere.

On success it returns what each field holds after the write, read back by the broker. These are facts about the field, never its value or any part of it:

```jsonc
{
"filled": true,
"confirmed": false, // true when every field holds its value
"secret_id": "…",
"element_uids": ["e12", "e13"],
"fields": [
// length: characters in the field now; matches: equals the secret (or its part),
// ignoring spaces and punctuation the page adds; valid: the field's
// validity.valid, when it has one
{
"element_uid": "e12",
"key": "number",
"length": 0,
"matches": false,
"valid": true,
},
{
"element_uid": "e13",
"key": "expiry",
"length": 4,
"matches": true,
"valid": false,
},
],
}
```

`matches: false` or `length: 0` means the page dropped or changed the value (some masked or script-controlled inputs do). `valid: false` means the page rejects the value's format. Report this to the user; do not submit the form as if the fill worked. A field the page removed during the fill has no `length` and `matches: false`.

Single-value secrets take `element_uid`. A JSON-payload secret β€” its binding declares `sbm:fields`, a map from payload key to required selector β€” takes `fields` instead, filling several inputs in ONE call: one export, one approval. Example: a card secret with keys `number`/`expiry`/`cvc` fills all three checkout fields at once. Such a secret refuses a plain `element_uid` fill, so its whole payload never lands in one field.

When the export needs multi-party approval, returns `{ filled: false, status: "pending_approval", fill_id, activity_id, approval_url? }` instead β€” see `await_fill`. `approval_url` is the fully qualified Turnkey dashboard link for that activity (absent on the mock backend); relay it to the user verbatim. Calling `fill_secret` again for the same secret and element returns the same `fill_id`.

## await_fill

`{ fill_id: string, timeout_seconds?: number (default 30, max 120), element_uid?: string, fields?: { key, element_uid }[] }`. Completes a pending fill once approvers reach quorum: waits up to the timeout, re-validates the destination against the live page, injects. Returns `pending_approval` again (same fields, including `approval_url`) if the timeout passes first β€” the fill stays valid, call again. Fails permanently if the export is rejected. If the target element left the page (or a hosted broker restarted), the fill stays valid: navigate back, snapshot, and call `await_fill` again with the same `fill_id` and the new `element_uid` or `fields`.

## list_network_requests (unavailable)

Registered scaffold only: returns `not_implemented`. Do not plan workflows around this tool.
`{ fill_id: string, timeout_seconds?: number (default 30, max 120), element_uid?: string, fields?: { key, element_uid }[] }`. Completes a pending fill once approvers reach quorum: waits up to the timeout, re-validates the destination against the live page, injects, and returns the same result as a completed `fill_secret`. Returns `pending_approval` again (same fields, including `approval_url`) if the timeout passes first β€” the fill stays valid, call again. Fails permanently if the export is rejected. If the target element left the page (or a hosted broker restarted), the fill stays valid: navigate back, snapshot, and call `await_fill` again with the same `fill_id` and the new `element_uid` or `fields`.
Loading
Loading