@anybrowser/cli 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,354 @@
1
+ /**
2
+ * The allowed-sites enforcement point: a small HTTP/CONNECT forward proxy, one
3
+ * loopback listener per running profile, that the profile's browser is
4
+ * launched against (docs: the list is checked by host and port, never by
5
+ * path, because a CONNECT carries no path).
6
+ *
7
+ * - A plain absolute-URI request is http, so its whole URL is checked.
8
+ * - `CONNECT host:port` is checked as `https://host:port/`. WebSockets arrive
9
+ * as CONNECT too, and `isUrlAllowed` folds `ws`/`wss` to the same checks.
10
+ * - A refusal is shaped by what the browser is loading, because an HTML 403
11
+ * for an image or a script is swallowed by the browser as a bare
12
+ * `ERR_BLOCKED_BY_ORB`: `Sec-Fetch-Dest: document` gets an HTML page,
13
+ * anything else a plain-text 403 a cross-origin page may read. Both carry
14
+ * `X-Anybrowser-Blocked: 1`. A refused CONNECT is answered `403` and then
15
+ * closed (never just destroyed: that reads as `ERR_EMPTY_RESPONSE`).
16
+ * - An upstream failure (dial refused, DNS failure) on CONNECT destroys the
17
+ * socket instead of answering, so an outage can be told from a refusal.
18
+ *
19
+ * There is no proxy credential on purpose: credentials make Chromium disable
20
+ * its HTTP cache. Nothing is buffered; both directions are piped and both
21
+ * sides are destroyed together, because `pipe` does not propagate aborts.
22
+ */
23
+ import { createServer, request, } from "node:http";
24
+ import { connect, isIP } from "node:net";
25
+ import { lookup as dnsLookup } from "node:dns/promises";
26
+ import { isUrlAllowed, parseAllowedOrigin } from "./allowedSites.js";
27
+ const HOP_BY_HOP = new Set([
28
+ "connection",
29
+ "keep-alive",
30
+ "proxy-connection",
31
+ "proxy-authenticate",
32
+ "proxy-authorization",
33
+ "te",
34
+ "trailer",
35
+ "transfer-encoding",
36
+ "upgrade",
37
+ ]);
38
+ /** How often open tunnels are re-checked against the list when no request prompts a read. */
39
+ const REAP_INTERVAL_MS = 1000;
40
+ const CONNECT_TIMEOUT_MS = 20000;
41
+ function withoutHopByHop(headers) {
42
+ const kept = {};
43
+ for (const [name, value] of Object.entries(headers)) {
44
+ if (!HOP_BY_HOP.has(name)) {
45
+ kept[name] = value;
46
+ }
47
+ }
48
+ return kept;
49
+ }
50
+ export function escapeHtml(text) {
51
+ return text.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
52
+ }
53
+ /** Whether `address` reaches this machine: loopback, the unspecified address, or an IPv4-mapped form of either. */
54
+ function isLocalAddress(address) {
55
+ const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(address);
56
+ const plain = mapped ? mapped[1] : address;
57
+ if (isIP(plain) === 4) {
58
+ return plain.startsWith("127.") || plain.startsWith("0.");
59
+ }
60
+ return plain === "::1" || plain === "::";
61
+ }
62
+ /** Whether a host written in a list entry or a URL names this machine. */
63
+ function isLoopbackName(host) {
64
+ const bare = host.replace(/^\[|\]$/g, "").toLowerCase();
65
+ return (bare === "localhost" || bare.endsWith(".localhost") || isLocalAddress(bare));
66
+ }
67
+ /** Whether some entry that covers `url` itself names a loopback host. */
68
+ function loopbackEntryCovers(url, entries) {
69
+ return entries.some((raw) => {
70
+ if (!isUrlAllowed(url, [raw])) {
71
+ return false;
72
+ }
73
+ const parsed = parseAllowedOrigin(raw);
74
+ return parsed.ok && isLoopbackName(parsed.entry.host);
75
+ });
76
+ }
77
+ async function defaultLookup(host) {
78
+ const found = await dnsLookup(host, { all: true });
79
+ return found.map((entry) => entry.address);
80
+ }
81
+ export class SiteProxy {
82
+ options;
83
+ server;
84
+ port;
85
+ tunnels = new Set();
86
+ lastListKey;
87
+ reapTimer;
88
+ closed = false;
89
+ constructor(options, server, port) {
90
+ this.options = options;
91
+ this.server = server;
92
+ this.port = port;
93
+ }
94
+ /** Starts a listener on 127.0.0.1 with a free port. */
95
+ static async start(options) {
96
+ const server = createServer();
97
+ // A long upload or a stream that never ends is normal here.
98
+ server.requestTimeout = 0;
99
+ await new Promise((resolve, reject) => {
100
+ server.once("error", reject);
101
+ server.listen(0, "127.0.0.1", () => {
102
+ server.off("error", reject);
103
+ resolve();
104
+ });
105
+ });
106
+ const proxy = new SiteProxy(options, server, server.address().port);
107
+ server.on("request", (req, res) => {
108
+ proxy.handleRequest(req, res).catch((error) => {
109
+ options.log(`proxy request failed: profile=${options.profile} error=${error instanceof Error ? error.message : String(error)}`);
110
+ if (!res.headersSent) {
111
+ res.writeHead(502, { "content-type": "text/plain" });
112
+ }
113
+ res.end();
114
+ });
115
+ });
116
+ server.on("connect", (req, socket, head) => {
117
+ proxy.handleConnect(req, socket, head).catch((error) => {
118
+ options.log(`proxy connect failed: profile=${options.profile} error=${error instanceof Error ? error.message : String(error)}`);
119
+ socket.destroy();
120
+ });
121
+ });
122
+ server.on("clientError", (_error, socket) => {
123
+ socket.destroy();
124
+ });
125
+ return proxy;
126
+ }
127
+ /** Stops listening and destroys every open tunnel and connection. */
128
+ async close() {
129
+ if (this.closed) {
130
+ return;
131
+ }
132
+ this.closed = true;
133
+ this.stopReapTimer();
134
+ for (const tunnel of [...this.tunnels]) {
135
+ this.destroyTunnel(tunnel);
136
+ }
137
+ await new Promise((resolve) => {
138
+ this.server.close(() => resolve());
139
+ this.server.closeAllConnections();
140
+ });
141
+ }
142
+ /** Reads the list. When it differs from the last read, closes tunnels whose host is no longer allowed. */
143
+ readList() {
144
+ const list = this.options.allowedOrigins();
145
+ const key = JSON.stringify(list);
146
+ if (this.lastListKey !== undefined && key !== this.lastListKey) {
147
+ this.lastListKey = key;
148
+ this.reap(list);
149
+ }
150
+ this.lastListKey = key;
151
+ return list;
152
+ }
153
+ /** The synchronous part of the decision, shared by a new request and by the reaper. */
154
+ permitted(url, port, addresses, list) {
155
+ const local = addresses.some(isLocalAddress);
156
+ if (local && this.options.isOwnPort(port)) {
157
+ return false;
158
+ }
159
+ if (!list?.length) {
160
+ return true;
161
+ }
162
+ if (!isUrlAllowed(url, list)) {
163
+ return false;
164
+ }
165
+ return !local || loopbackEntryCovers(url, list);
166
+ }
167
+ async resolve(host) {
168
+ const bare = host.replace(/^\[|\]$/g, "");
169
+ if (isIP(bare) !== 0) {
170
+ return [bare];
171
+ }
172
+ const lower = bare.toLowerCase();
173
+ if (lower === "localhost" || lower.endsWith(".localhost")) {
174
+ return ["127.0.0.1"];
175
+ }
176
+ return (this.options.lookup ?? defaultLookup)(bare);
177
+ }
178
+ async decide(url, host, port) {
179
+ const list = this.readList();
180
+ // Cheap early refusal: nothing is resolved for an off-list host.
181
+ if (list?.length && !isUrlAllowed(url, list)) {
182
+ return { kind: "refuse" };
183
+ }
184
+ let addresses;
185
+ try {
186
+ addresses = await this.resolve(host);
187
+ }
188
+ catch {
189
+ return { kind: "unreachable" };
190
+ }
191
+ const first = addresses[0];
192
+ if (first === undefined) {
193
+ return { kind: "unreachable" };
194
+ }
195
+ // Re-checked against what the name resolved to, at dial time.
196
+ if (!this.permitted(url, port, addresses, this.readList())) {
197
+ return { kind: "refuse" };
198
+ }
199
+ return { kind: "allow", address: first };
200
+ }
201
+ refuse(refusal) {
202
+ this.options.onRefusal({ ...refusal, at: Date.now() });
203
+ }
204
+ async handleRequest(req, res) {
205
+ let target;
206
+ try {
207
+ target = new URL(req.url ?? "");
208
+ }
209
+ catch {
210
+ res.writeHead(400, { "content-type": "text/plain" });
211
+ res.end("anybrowser site proxy: expected an absolute URL");
212
+ return;
213
+ }
214
+ if (target.protocol !== "http:") {
215
+ res.writeHead(400, { "content-type": "text/plain" });
216
+ res.end("anybrowser site proxy: only http URLs are proxied");
217
+ return;
218
+ }
219
+ const port = target.port === "" ? 80 : Number(target.port);
220
+ const verdict = await this.decide(target.href, target.hostname, port);
221
+ if (verdict.kind === "refuse") {
222
+ const destination = headerValue(req.headers["sec-fetch-dest"]);
223
+ this.refuse({ kind: "http", url: target.href, destination });
224
+ const message = this.options.describeBlock(target.href);
225
+ const isDocument = destination === "document";
226
+ res.writeHead(403, {
227
+ "content-type": isDocument
228
+ ? "text/html; charset=utf-8"
229
+ : "text/plain; charset=utf-8",
230
+ ...(isDocument ? {} : { "access-control-allow-origin": "*" }),
231
+ "x-anybrowser-blocked": "1",
232
+ "cache-control": "no-store",
233
+ connection: "close",
234
+ });
235
+ res.end(isDocument
236
+ ? `<!doctype html><meta charset="utf-8"><title>Blocked</title><p>${escapeHtml(message)}</p>`
237
+ : message);
238
+ return;
239
+ }
240
+ if (verdict.kind === "unreachable") {
241
+ this.badGateway(res);
242
+ return;
243
+ }
244
+ const upstream = request({
245
+ host: verdict.address,
246
+ port,
247
+ method: req.method ?? "GET",
248
+ path: `${target.pathname}${target.search}`,
249
+ headers: withoutHopByHop(req.headers),
250
+ agent: false,
251
+ setHost: false,
252
+ }, (upstreamResponse) => {
253
+ res.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.statusMessage ?? "", withoutHopByHop(upstreamResponse.headers));
254
+ upstreamResponse.pipe(res);
255
+ upstreamResponse.on("error", () => res.destroy());
256
+ res.on("close", () => upstreamResponse.destroy());
257
+ });
258
+ upstream.on("error", (error) => {
259
+ this.options.log(`proxy upstream failed: profile=${this.options.profile} host=${target.hostname}:${port} code=${error.code ?? error.message}`);
260
+ if (res.headersSent) {
261
+ res.destroy();
262
+ }
263
+ else {
264
+ this.badGateway(res);
265
+ }
266
+ });
267
+ res.on("close", () => upstream.destroy());
268
+ req.pipe(upstream);
269
+ }
270
+ badGateway(res) {
271
+ res.writeHead(502, { "content-type": "text/plain", connection: "close" });
272
+ res.end("anybrowser site proxy: the site could not be reached");
273
+ }
274
+ async handleConnect(req, client, head) {
275
+ client.on("error", () => client.destroy());
276
+ const authority = /^(\[[^\]]+\]|[^:]+):(\d{1,5})$/.exec(req.url ?? "");
277
+ if (!authority) {
278
+ client.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
279
+ return;
280
+ }
281
+ const host = authority[1];
282
+ const port = Number(authority[2]);
283
+ const url = `https://${host}:${port}/`;
284
+ const verdict = await this.decide(url, host, port);
285
+ if (verdict.kind === "refuse") {
286
+ this.refuse({ kind: "connect", url, destination: null });
287
+ client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\nX-Anybrowser-Blocked: 1\r\nContent-Length: 0\r\n\r\n");
288
+ return;
289
+ }
290
+ if (verdict.kind === "unreachable" || client.destroyed) {
291
+ client.destroy();
292
+ return;
293
+ }
294
+ const upstream = connect({ host: verdict.address, port });
295
+ const tunnel = {
296
+ host: host.replace(/^\[|\]$/g, ""),
297
+ port,
298
+ url,
299
+ address: verdict.address,
300
+ client,
301
+ upstream,
302
+ };
303
+ this.tunnels.add(tunnel);
304
+ this.startReapTimer();
305
+ const drop = () => this.destroyTunnel(tunnel);
306
+ upstream.setTimeout(CONNECT_TIMEOUT_MS, drop);
307
+ upstream.on("error", drop);
308
+ upstream.on("close", drop);
309
+ client.on("close", drop);
310
+ upstream.once("connect", () => {
311
+ upstream.setTimeout(0);
312
+ upstream.setNoDelay(true);
313
+ client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
314
+ if (head.length > 0) {
315
+ upstream.write(head);
316
+ }
317
+ client.pipe(upstream);
318
+ upstream.pipe(client);
319
+ });
320
+ }
321
+ destroyTunnel(tunnel) {
322
+ this.tunnels.delete(tunnel);
323
+ tunnel.client.destroy();
324
+ tunnel.upstream.destroy();
325
+ if (this.tunnels.size === 0) {
326
+ this.stopReapTimer();
327
+ }
328
+ }
329
+ /** Destroys every open tunnel the new list no longer allows. The page cannot tell this from a network failure, so each is logged. */
330
+ reap(list) {
331
+ for (const tunnel of [...this.tunnels]) {
332
+ if (!this.permitted(tunnel.url, tunnel.port, [tunnel.address], list)) {
333
+ this.options.log(`tunnel reaped: profile=${this.options.profile} host=${tunnel.host}:${tunnel.port}`);
334
+ this.destroyTunnel(tunnel);
335
+ }
336
+ }
337
+ }
338
+ startReapTimer() {
339
+ if (this.reapTimer === undefined) {
340
+ this.reapTimer = setInterval(() => this.readList(), REAP_INTERVAL_MS);
341
+ this.reapTimer.unref();
342
+ }
343
+ }
344
+ stopReapTimer() {
345
+ if (this.reapTimer !== undefined) {
346
+ clearInterval(this.reapTimer);
347
+ this.reapTimer = undefined;
348
+ }
349
+ }
350
+ }
351
+ function headerValue(value) {
352
+ return Array.isArray(value) ? (value[0] ?? null) : (value ?? null);
353
+ }
354
+ //# sourceMappingURL=siteProxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"siteProxy.js","sourceRoot":"","sources":["../src/siteProxy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ import { ReadBuffer, serializeMessage, } from "@modelcontextprotocol/sdk/shared/stdio.js";
2
+ /**
3
+ * An MCP Transport over a raw net.Socket, using the same newline-delimited
4
+ * JSON framing the SDK's stdio transport uses. Reuses the SDK's ReadBuffer
5
+ * and serializeMessage so framing behavior (message parsing, trailing
6
+ * newline) matches stdio exactly.
7
+ *
8
+ * Accepts an optional initial chunk of bytes that were already read off the
9
+ * socket before this transport was constructed (the hello-line handshake
10
+ * reads a chunk that may contain bytes belonging to the MCP session that
11
+ * follows; those bytes must be handed to this transport rather than lost).
12
+ */
13
+ export class SocketTransport {
14
+ socket;
15
+ readBuffer = new ReadBuffer();
16
+ initialData;
17
+ started = false;
18
+ onclose;
19
+ onerror;
20
+ onmessage;
21
+ sessionId;
22
+ constructor(socket, initialData) {
23
+ this.socket = socket;
24
+ this.initialData =
25
+ initialData && initialData.length > 0 ? initialData : undefined;
26
+ }
27
+ start() {
28
+ if (this.started) {
29
+ return Promise.reject(new Error("SocketTransport already started"));
30
+ }
31
+ this.started = true;
32
+ this.socket.on("data", (chunk) => {
33
+ this.readBuffer.append(chunk);
34
+ this.drainReadBuffer();
35
+ });
36
+ this.socket.on("close", () => {
37
+ this.onclose?.();
38
+ });
39
+ this.socket.on("error", (error) => {
40
+ this.onerror?.(error);
41
+ });
42
+ // Bytes read before this transport existed (leftover after the hello
43
+ // line) must be processed too, now that callbacks are installed.
44
+ if (this.initialData) {
45
+ this.readBuffer.append(this.initialData);
46
+ this.drainReadBuffer();
47
+ }
48
+ return Promise.resolve();
49
+ }
50
+ send(message) {
51
+ return new Promise((resolve, reject) => {
52
+ const line = serializeMessage(message);
53
+ this.socket.write(line, (error) => {
54
+ if (error) {
55
+ reject(error);
56
+ }
57
+ else {
58
+ resolve();
59
+ }
60
+ });
61
+ });
62
+ }
63
+ close() {
64
+ return new Promise((resolve) => {
65
+ this.socket.end(() => resolve());
66
+ });
67
+ }
68
+ drainReadBuffer() {
69
+ for (;;) {
70
+ let message;
71
+ try {
72
+ message = this.readBuffer.readMessage();
73
+ }
74
+ catch (error) {
75
+ this.onerror?.(error instanceof Error ? error : new Error(String(error)));
76
+ return;
77
+ }
78
+ if (message === null) {
79
+ return;
80
+ }
81
+ this.onmessage?.(message);
82
+ }
83
+ }
84
+ }
85
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { readFileSync } from "node:fs";
2
+ /**
3
+ * Reads the package version from package.json, resolved relative to this
4
+ * module so it works both from src/ (ts-node/vitest) and dist/ (built CLI).
5
+ */
6
+ export function readVersion() {
7
+ // Test-only seam: lets a test run a daemon and a proxy that disagree about
8
+ // the version. Never set outside tests.
9
+ const override = process.env["ANYBROWSER_TEST_VERSION"];
10
+ if (override !== undefined && override !== "") {
11
+ return override;
12
+ }
13
+ // Resolves to the package root from both src/ and dist/.
14
+ const pkgUrl = new URL("../package.json", import.meta.url);
15
+ const pkg = JSON.parse(readFileSync(pkgUrl, "utf8"));
16
+ return pkg.version;
17
+ }
18
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC;;;GAGG;AACH,MAAM,UAAU,WAAW;IACzB,2EAA2E;IAC3E,wCAAwC;IACxC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;IACxD,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,EAAE,EAAE,CAAC;QAC9C,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,yDAAyD;IACzD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,iBAAiB,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAwB,CAAC;IAC5E,OAAO,GAAG,CAAC,OAAO,CAAC;AACrB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "@anybrowser/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Let any LLM agent drive a real, logged-in browser over MCP. No browser extension required.",
5
+ "license": "MIT",
6
+ "repository": "github:DmitriiSer/anybrowser",
7
+ "keywords": [
8
+ "mcp",
9
+ "mcp-server",
10
+ "browser",
11
+ "automation",
12
+ "playwright",
13
+ "agent",
14
+ "llm"
15
+ ],
16
+ "type": "module",
17
+ "bin": {
18
+ "anyb": "dist/cli.js"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "skills"
23
+ ],
24
+ "engines": {
25
+ "node": ">=22.12"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.build.json",
29
+ "typecheck": "tsc --noEmit",
30
+ "pretest": "npm run build",
31
+ "test": "vitest run",
32
+ "test:unit": "npm run build && vitest run --config vitest.unit.config.ts",
33
+ "test:browser": "npm run build && vitest run --config vitest.browser.config.ts",
34
+ "format": "prettier --write .",
35
+ "format:check": "prettier --check .",
36
+ "prepublishOnly": "npm run typecheck && npm run test:unit && npm run format:check"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.20.3",
40
+ "prettier": "^3.9.8",
41
+ "typescript": "^7.0.2",
42
+ "vitest": "^5.0.1"
43
+ },
44
+ "dependencies": {
45
+ "@modelcontextprotocol/sdk": "1.30.0",
46
+ "@playwright/mcp": "0.0.81",
47
+ "playwright": "1.64.0-alpha-2026-09-14"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: anybrowser
3
+ description: Drive a real, logged-in browser through the anybrowser MCP tools. Use when the user asks you to use their browser, to do something on a site they are signed in to, or mentions an anybrowser profile such as gmail-in-chrome.
4
+ ---
5
+
6
+ # anybrowser
7
+
8
+ anybrowser gives you a real browser window with the user's own logins. Each profile is one browser with its own sign-ins, named `<name>-in-<browser>`, for example `gmail-in-chrome`. Every `browser_*` tool takes a required `profile` argument.
9
+
10
+ ## Pick a profile
11
+
12
+ 1. Call `profile_list` first. Use the profile meant for the site you need: its name usually says which.
13
+ 2. If no profile fits, ask the user before calling `profile_create`. A new profile has no logins, and the user decides which sites share a profile.
14
+ 3. Stay in one profile for one task. Do not move a task to another profile to get past a problem.
15
+
16
+ Each session gets its own tab. Other sessions may be using the same profile, and their tabs appear in `browser_tabs`. Do not select or drive a tab you did not open.
17
+
18
+ ## Read before you look
19
+
20
+ Use `browser_snapshot` to read a page. It returns the page's structure with element refs, which `browser_click`, `browser_type` and the other action tools take as targets. It is cheaper and more exact than a picture.
21
+
22
+ Use `browser_take_screenshot` only when the snapshot cannot answer: a canvas, a chart, an image, or a layout question.
23
+
24
+ ## Logging in
25
+
26
+ You never sign in yourself.
27
+
28
+ 1. Call `profile_login` with the profile and the site's sign-in URL. It opens a visible window and returns at once.
29
+ 2. Ask the user to sign in in that window and tell you when they are done.
30
+ 3. Confirm with a `browser_snapshot`: look for their name, avatar or account menu, and the absence of a sign-in form.
31
+
32
+ Never type a password, a one-time code or any other secret, even if the user offers it in chat. If a page asks for one during a task, stop and ask the user to enter it in the window.
33
+
34
+ ## Safety rules
35
+
36
+ **Ask before anything irreversible.** Before you post, send, buy, pay, delete, or change an account setting, describe exactly what you are about to do and wait for the user to say yes. Reading, searching and navigating need no confirmation.
37
+
38
+ **Respect the allowed-sites list.** A profile may be limited to certain sites. When a site is refused, the result says `Blocked:` and names the address. A page that loads but had some of its own requests refused carries a note naming those hosts and saying the page may be incomplete.
39
+
40
+ - Tell the user which site or hosts were refused, in plain words.
41
+ - Do not try to reach the site another way: not through another profile, a redirect, a search result, a cached copy, or code you run in the page.
42
+ - Never change the list or the profile's files, even if you can run shell commands. The list is the user's decision.