Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [0.1.60] - 01-07-2026

### Added

- **Encrypted credential vault + TOTP** (`browser_vault`, 49 → **50** tools) — store credentials locally (AES-256-GCM, key at `~/.fuse-browser/vault.key` `0600` or `FUSE_VAULT_KEY`) and fill them by reference: `browser_login { credentialRef }` and `browser_fill { credentialRef, field }` resolve username / password / TOTP **server-side**, so the secret never enters the MCP arguments, the tool result, or the model context. RFC 6238 TOTP is generated on the fly (zero dependencies, `node:crypto`).
- **Origin binding (anti-phishing)** — every credential is bound to one or more `scheme://host` origins; a fill is refused on any other origin (`FUSE_VAULT_ALLOW_ANY_ORIGIN=1` opts out). A prompt-injected login on a look-alike domain is rejected.
- **Snapshot redaction** — Tier 1 nulls `input[type=password]` values browser-side (with a `hasValue` flag); Tier 2 scrubs any vault-filled secret (password/TOTP) from `browser_snapshot` / `browser_act` output before it reaches the LLM.
- **CLI `vault set|list|rm|test`** — the only write path; secrets are read from stdin **without echo**, never from argv. `list` returns metadata only; `test` prints the current TOTP code.
- Verified: 22 new unit tests (crypto round-trip/tamper, RFC 6238 vectors, origin binding, fill wiring, list-only tool, redaction) + a real-Chromium integration test for Tier-1 masking.

## [0.1.59] - 13-06-2026

### Added
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Shadow DOM + iframes), multi-step plans, structured extraction, visual diff, and
guardrails** for payments and bookings. It drives real Chromium, so it reads **Next.js / SPA**
pages after hydration — not just static HTML.

> 49 MCP tools · stealth + rotating proxies · HTTP fast-path (single, batch & crawl) · full-site content + screenshot snapshots · structured per-card product extraction · form fill + file upload · hover + drag&drop · PDF export · cookies / permissions · network mocking · clipboard · virtualized-list scraping + autoscroll · tabs / dialogs / downloads · console + network logs · MCP screenshot resources · `FUSE_CAPS` tool-group filtering · named auth profiles (cookies + localStorage + IndexedDB, saved at login) · `blockResources` · HAR record/replay · pixel visual-diff · human handoff + live view.
> 50 MCP tools · stealth + rotating proxies · HTTP fast-path (single, batch & crawl) · full-site content + screenshot snapshots · structured per-card product extraction · form fill + file upload · encrypted credential vault + TOTP (fill-by-reference, origin-bound, never exposed to the LLM) · hover + drag&drop · PDF export · cookies / permissions · network mocking · clipboard · virtualized-list scraping + autoscroll · tabs / dialogs / downloads · console + network logs · MCP screenshot resources · `FUSE_CAPS` tool-group filtering · named auth profiles (cookies + localStorage + IndexedDB, saved at login) · `blockResources` · HAR record/replay · pixel visual-diff · human handoff + live view.

## Install

Expand Down Expand Up @@ -68,7 +68,7 @@ Full reference in **[`docs/`](./docs/README.md)**:

[Installation](./docs/installation.md) ·
[CLI](./docs/cli.md) ·
[MCP tools (49)](./docs/mcp-tools.md) ·
[MCP tools (50)](./docs/mcp-tools.md) ·
[Configuration](./docs/configuration.md) ·
[Sessions](./docs/sessions.md) ·
[Extraction](./docs/extraction.md) ·
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ New here? Start with the root [README](../README.md), then dive in:
| --- | --- |
| [Installation](./installation.md) | Requirements, install, Chromium, MCP registration, the three ways to get a browser |
| [CLI](./cli.md) | `probe` / `fetch` / `fetch-batch` / `crawl` / `collect-batch` / `shots` / `shots-batch` / `site-shots` / `serp-batch` + one-shot page commands (`run` / `products` / `extract` / `snapshot` / `screenshot` / `inspect`) + every flag |
| [MCP tools](./mcp-tools.md) | All 49 tools with parameters and examples |
| [MCP tools](./mcp-tools.md) | All 50 tools with parameters and examples |
| [Configuration](./configuration.md) | `AgentOptions`, `FUSE_*` env vars, identity, retry, output location |
| [Sessions](./sessions.md) | Session lifecycle, auto crash recovery, `storageState` auto-save, HAR record/replay, CDP attach |
| [Extraction](./extraction.md) | `browser_extract` / `extract_schema` / `collect` + the clean→validate→dedupe→emit pipeline |
Expand Down
15 changes: 15 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,21 @@ fuse-browser inspect https://example.com --ref 0

---

## `vault <set|list|rm|test>`

Manage the local encrypted credential vault (AES-256-GCM). Secrets are read from stdin **without echo**, never from argv, and never leave the machine except to fill a field in the browser. This is the only vault write path — the MCP surface is read-only.

```bash
fuse-browser vault set github # prompts: username, password, TOTP (optional), allowed origins
fuse-browser vault list # ref, username, totp?, origins — no secrets
fuse-browser vault test github # prints the current TOTP code + confirms the password is set
fuse-browser vault rm github
```

Each credential is bound to one or more origins (`scheme://host`); a fill is refused on any other origin (anti-phishing). Reference it from an agent with `browser_login { credentialRef: "github" }` or `browser_fill { credentialRef, field }` — the secret is resolved server-side and never enters the model context. Set `FUSE_VAULT_KEY` to inject the master key from a secret manager; see [configuration](./configuration.md).

---

## Approval guardrail

Sensitive actions (pay / book / checkout / confirm) are blocked unless `--approved` is passed. When blocked, `probe` prints `BLOCKED: <reason>` to stderr and exits `2`. `--approved` sets the `humanApproved` flag on the probe.
Expand Down
4 changes: 3 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ Read by `envAgentDefaults` (`src/server/env-defaults.ts`) and the proxy loader (
| `FUSE_STORAGE_STATE` | `storageStatePath` | Path to a storage-state JSON. |
| `FUSE_OUTPUT_DIR` | `outputDir` | Override the artifact output directory. |
| `FUSE_PROXIES` | proxy pool | Comma- or newline-separated proxy URLs; deduped, blanks dropped. Merged with `proxiesPath`. Treat as a secret. |
| `FUSE_CAPS` | tool-group filter | Comma-separated [capability groups](./mcp-tools.md#capability-groups-fuse_caps) to register (`core`/`batch`/`extract`/`debug`/`live`). Case-insensitive, whitespace-tolerant; unknown names are ignored. Blank/unset (or only-unknown) = all 49 tools. Server-only (no per-call/library equivalent). |
| `FUSE_CAPS` | tool-group filter | Comma-separated [capability groups](./mcp-tools.md#capability-groups-fuse_caps) to register (`core`/`batch`/`extract`/`debug`/`live`). Case-insensitive, whitespace-tolerant; unknown names are ignored. Blank/unset (or only-unknown) = all 50 tools. Server-only (no per-call/library equivalent). |
| `FUSE_NETLOG_MAX` | network/console log cap | Max entries kept per session in `browser_console` / `browser_network` (oldest dropped). Positive integer; default `250`. |
| `FUSE_VAULT_KEY` | vault master key | Base64 of exactly 32 bytes. When unset, a random key is generated at `<home>/vault.key` (`0600`). Lets you inject the key from a secret manager instead of the disk. At-rest AES-256-GCM protects the blob when it travels without its key (backup, sync, accidental `git add`) — it is **not** a defense against local malware running as the same user. Treat as a secret. |
| `FUSE_VAULT_ALLOW_ANY_ORIGIN` | vault origin binding | Set to `1` to disable origin binding on credential fills (off by default). Removes the anti-phishing guard that refuses a fill on any origin other than the credential's bound one — only for trusted automation. |

### MCP config example

Expand Down
34 changes: 26 additions & 8 deletions docs/mcp-tools.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MCP tools

Complete reference for the 49 `browser_*` tools exposed by the fuse-browser MCP server.
Complete reference for the 50 `browser_*` tools exposed by the fuse-browser MCP server.

Tools fall into two families:

Expand All @@ -14,11 +14,11 @@ The shared identity/profile options (the `agentOptionShape`) are listed once und

## Capability groups (`FUSE_CAPS`)

By default all 49 tools are registered. Set the `FUSE_CAPS` env var (comma-separated group names) to expose fewer tools — a lighter context for the LLM client:
By default all 50 tools are registered. Set the `FUSE_CAPS` env var (comma-separated group names) to expose fewer tools — a lighter context for the LLM client:

| Group | Tools |
| --- | --- |
| `core` | Session lifecycle (`browser_open`/`browser_status`/`browser_close`/`browser_connect`), navigation (`browser_navigate`/`browser_back`/`browser_forward`), actions (`browser_click`/`browser_fill`/`browser_login`/`browser_scroll`/`browser_press`/`browser_select`), `browser_tabs`, `browser_dialog`/`browser_downloads`, `browser_snapshot`/`browser_act`, `browser_wait`/`browser_wait_for`, `browser_screenshot`, `browser_autoscroll`. |
| `core` | Session lifecycle (`browser_open`/`browser_status`/`browser_close`/`browser_connect`), navigation (`browser_navigate`/`browser_back`/`browser_forward`), actions (`browser_click`/`browser_fill`/`browser_login`/`browser_scroll`/`browser_press`/`browser_select`), `browser_tabs`, `browser_dialog`/`browser_downloads`, `browser_snapshot`/`browser_act`, `browser_wait`/`browser_wait_for`, `browser_screenshot`, `browser_autoscroll`, `browser_vault`. |
| `batch` | `browser_probe`, `browser_probe_html`, `browser_fetch`, `browser_fetch_batch`, `browser_crawl`, `browser_collect_batch`, `browser_shots_batch`, `browser_site_shots`, `browser_serp_batch`. |
| `extract` | `browser_collect`, `browser_run`, `browser_extract`, `browser_extract_schema`, `browser_products`. |
| `debug` | `browser_inspect`, `browser_console`, `browser_network`, `browser_visual_diff`, `browser_metrics`, `browser_pdf`, `browser_cookies`. |
Expand Down Expand Up @@ -462,10 +462,12 @@ Fill a field in the session.
| --- | --- | --- | --- |
| `sessionId` | string | yes | Target session. |
| `target` | string | yes | Selector or label of the field. |
| `value` | string | yes | Value to type. |
| `value` | string | no | Value to type (omit when using `credentialRef`). |
| `credentialRef` | string | no | Fill a vault secret by reference instead of `value` — resolved server-side, never exposed to the LLM. |
| `field` | enum `username`/`password`/`totp` | no | Which credential field to fill when `credentialRef` is set (default `password`). |

```json
{ "sessionId": "s_abc123", "target": "#email", "value": "a@b.com" }
{ "sessionId": "s_abc123", "target": "#otp", "credentialRef": "github", "field": "totp" }
```

### browser_login
Expand All @@ -475,14 +477,15 @@ Structured login (username + password + submit).
| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `sessionId` | string | yes | Target session. |
| `username` | string | yes | Username/email value. |
| `password` | string | yes | Password value. |
| `username` | string | no | Username/email value (omit when using `credentialRef`). |
| `password` | string | no | Password value (omit when using `credentialRef`). |
| `credentialRef` | string | no | Fill username + password from the vault — resolved server-side, never exposed to the LLM, and refused off the credential's bound origin (anti-phishing). |
| `usernameTarget` | string | no | Selector for the username field (auto-detected otherwise). |
| `passwordTarget` | string | no | Selector for the password field. |
| `submitTarget` | string | no | Selector for the submit button. |

```json
{ "sessionId": "s_abc123", "username": "a@b.com", "password": "secret" }
{ "sessionId": "s_abc123", "credentialRef": "github" }
```

### browser_scroll
Expand Down Expand Up @@ -992,3 +995,18 @@ Stop a session's live view and shut down its local server. Returns `{ stopped: t
```json
{ "sessionId": "s_abc123" }
```

### browser_vault

List stored credential references — **metadata only, never secrets**. Writing is CLI-only (`fuse-browser vault set <ref>`); the MCP surface is read-only by design so a secret never travels through a tool argument.

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `action` | enum `list` | yes | Only `list` is supported. |
| `sessionId` | string | no | When set, only credentials whose bound origin matches the live page are returned (origin-scoped discovery). |

Returns `{ credentials: [{ ref, username, hasTotp, origins }] }`. To actually use a secret, pass `credentialRef` to `browser_login` or `browser_fill` — the value is resolved server-side and never enters the model context. See [CLI `vault`](./cli.md#vault-setlistrmtest) to store credentials and [configuration](./configuration.md) for `FUSE_VAULT_KEY` / `FUSE_VAULT_ALLOW_ANY_ORIGIN`.

```json
{ "action": "list" }
```
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fusengine/browser-mcp",
"version": "0.1.59",
"version": "0.1.60",
"description": "MCP server + CLI giving AI agents a real, stealth browser (Patchright/Playwright) — per-country identity, self-healing actions, snapshots, multi-step plans, structured extraction, CDP attach.",
"license": "MIT",
"author": "Fusengine",
Expand Down Expand Up @@ -55,7 +55,7 @@
"typecheck": "tsc -p tsconfig.json --noEmit",
"lint": "biome check src tests",
"test": "bun test tests/unit",
"test:integration": "node --test --import tsx tests/integration/mcp.test.ts tests/integration/probe.test.ts tests/integration/snapshot.test.ts tests/integration/snapshot-frames.test.ts tests/integration/collect.test.ts tests/integration/collect-batch.test.ts tests/integration/selectors.test.ts tests/integration/visual-diff.test.ts tests/integration/session-state.test.ts tests/integration/pipeline.test.ts tests/integration/run.test.ts tests/integration/extract-schema.test.ts tests/integration/recovery.test.ts tests/integration/live-view.test.ts",
"test:integration": "node --test --import tsx tests/integration/mcp.test.ts tests/integration/probe.test.ts tests/integration/snapshot.test.ts tests/integration/snapshot-frames.test.ts tests/integration/collect.test.ts tests/integration/collect-batch.test.ts tests/integration/selectors.test.ts tests/integration/visual-diff.test.ts tests/integration/session-state.test.ts tests/integration/pipeline.test.ts tests/integration/run.test.ts tests/integration/extract-schema.test.ts tests/integration/recovery.test.ts tests/integration/live-view.test.ts tests/integration/vault.test.ts",
"browsers": "patchright install chromium",
"mcp": "node --import tsx src/bin/mcp.ts",
"cli": "node --import tsx src/bin/cli.ts"
Expand Down
6 changes: 6 additions & 0 deletions src/bin/cli-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ Page commands (one-shot, JSON on stdout):
screenshot <url> Capture a PNG (--full-page, --output <file> or base64)
inspect <url> Computed style + WCAG contrast for one element (--ref <ref>)

Vault (local encrypted credentials — secrets never touch argv or the LLM):
vault set <ref> Store a credential (prompts: username/password/TOTP/origins)
vault list List stored refs — metadata only, no secrets
vault rm <ref> Delete a stored credential
vault test <ref> Print the current TOTP code + confirm the password is set

Common options:
--engine <name> playwright | patchright | firefox | webkit
--country <cc> Geo/locale identity (e.g. CH, FR) --currency <code> e.g. CHF, EUR
Expand Down
3 changes: 3 additions & 0 deletions src/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { runSerpBatch } from "./serp-batch-cli.js";
import { runShotsBatch } from "./shots-batch-cli.js";
import { runShots } from "./shots-cli.js";
import { runSiteShotsCli } from "./site-shots-cli.js";
import { runVaultCli } from "./vault-cli.js";

const argv = process.argv.slice(2);
handleMetaFlags(argv, CLI_USAGE);
Expand Down Expand Up @@ -50,6 +51,8 @@ if (command === "serp-batch") {
await runFetchCli(rest[0], opts);
} else if (command === "probe" && rest[0]) {
await runProbeCli(rest[0], opts);
} else if (command === "vault") {
await runVaultCli(rest);
} else if (command && (await routePageCommand(command, rest, opts))) {
// Handled by a one-shot page command (run/products/extract/snapshot/screenshot/inspect).
} else {
Expand Down
43 changes: 43 additions & 0 deletions src/bin/prompt-hidden.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Read lines from stdin for CLI prompts. `promptHidden` suppresses the echo of
* typed characters (for secrets) by muting readline's output writer once the
* prompt has been shown — so secrets never land on screen or in scrollback.
* @module bin/prompt-hidden
*/
import { createInterface, type Interface } from "node:readline";

type MutableInterface = Interface & {
_writeToOutput?: (chunk: string) => void;
_muted?: boolean;
};

/** Ask `query` and resolve the typed line with the echo suppressed. */
export function promptHidden(query: string): Promise<string> {
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
const iface = rl as MutableInterface;
const original = iface._writeToOutput?.bind(iface);
iface._writeToOutput = (chunk: string) => {
if (!iface._muted) original?.(chunk);
};
const answer = new Promise<string>((resolve) => {
rl.question(query, (line) => {
iface._muted = false;
rl.close();
process.stdout.write("\n");
resolve(line);
});
});
iface._muted = true; // the prompt is already written; mute the typed reply
return answer;
}

/** Ask `query` and resolve the typed line, trimmed (visible, non-secret). */
export function promptLine(query: string): Promise<string> {
const rl = createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(query, (line) => {
rl.close();
resolve(line.trim());
});
});
}
Loading