browser-broker 0.1.0 → 0.3.1

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.
@@ -14,13 +14,26 @@ import { TOOLS_BY_NAME, TOOL_DEFINITIONS } from "./tools.js";
14
14
  *
15
15
  * Read once at module load rather than per handshake: it cannot change while
16
16
  * the process runs, and a session serves one client.
17
+ *
18
+ * ── Why the read is allowed to fail ─────────────────────────────────────
19
+ *
20
+ * A missing or unreadable manifest must not take the surface down with it.
21
+ * This module is imported by the executable a client spawns, so a throw here
22
+ * happens *before* anything can be served and before the protocol exists to
23
+ * describe it — the client sees the process exit, not an error it can act on.
24
+ * Weighed against that, the cost of not knowing the version is a cosmetic
25
+ * field in one handshake.
26
+ *
27
+ * The command line degrades this way already, and by accident rather than by
28
+ * design: it reads the manifest inside the function behind `--version`, so
29
+ * every other command keeps working when the file is gone. Matching that
30
+ * behaviour deliberately is the point of the `catch` — the fallback is the
31
+ * same literal this constant held before it read anything.
17
32
  */
18
- const manifest = await import('../../package.json', { with: { type: 'json' } });
33
+ const manifest = await import('../../package.json', { with: { type: 'json' } }).catch(() => undefined);
19
34
  export const SERVER_INFO = {
20
35
  name: 'browser-broker',
21
- version: typeof manifest.default.version === 'string'
22
- ? manifest.default.version
23
- : '0.0.0',
36
+ version: typeof manifest?.default?.version === 'string' ? manifest.default.version : '0.0.0',
24
37
  };
25
38
  /**
26
39
  * What `initialize` returns: the negotiated revision, what this server can
@@ -1,5 +1,136 @@
1
1
  import { OPERATION_NAMES } from "../adapter/operations.js";
2
2
  import { BROWSER_CHOICE_GUIDANCE } from "../browser/driver.js";
3
+ /**
4
+ * The twelve tools, their descriptions, and their argument schemas.
5
+ *
6
+ * ── Surface area is a standing tax, and this file is where it is paid ────
7
+ *
8
+ * `SCHEMA.md` §3.1 opens with it: every description here sits in a connected
9
+ * session's context **on every turn**, whether or not anything calls the
10
+ * tool. Twelve descriptions is the whole agent-facing documentation of this
11
+ * service and it is also a per-turn cost on every session, so each one is
12
+ * written to be the shortest text that still prevents a wrong call.
13
+ *
14
+ * **The description is the only place a calling agent reliably reads.** Not
15
+ * `SCHEMA.md`, not a wiki, not a refusal it has not hit yet. So where a fact
16
+ * changes what a caller does — that `browser_status` is also the renew verb,
17
+ * that the browser choice has no default, that feedback needs no lease — the
18
+ * fact is in the description rather than only in the argument list.
19
+ *
20
+ * **`browser_sign_in`'s description is the strongest case of that rule, and
21
+ * the reason it reads as an instruction rather than a summary.** The failure
22
+ * it exists to end is a caller hitting a login wall and never learning it
23
+ * could ask — and **no refusal can reach that caller**, because it never
24
+ * makes a call to be refused. It abandons the task or fabricates a session
25
+ * instead, which is what 25 measured sessions did in a month. There is no
26
+ * second surface for that guidance to live on, so the description names the
27
+ * alternative outright, the way a refusal would.
28
+ *
29
+ * ── What is deliberately absent ─────────────────────────────────────────
30
+ *
31
+ * **There is no browser-scoped destructive verb, and there must never be
32
+ * one.** No close-browser, no kill-all, no restart. `SCHEMA.md` §3.13 makes
33
+ * that part of the contract rather than an omission: the administrative
34
+ * operations act on something every caller shares, so they are commands a
35
+ * person runs (§4.3, §5.4) and the ledger records that a person did.
36
+ * **The worst thing an agent can do through this surface is close its own
37
+ * tab**, and that ceiling is the reason this surface can be handed to an
38
+ * arbitrary caller at all.
39
+ *
40
+ * **`browser_sign_in` and `browser_sign_in_done` move a browser's state and
41
+ * do not breach that ceiling**, which is worth stating because they look like
42
+ * the exception. Both are keyed, and both act only on the lease that called
43
+ * them: the first takes the browser only when no *other* lease holds a tab on
44
+ * it, and the second is refused unless the calling lease is the one that
45
+ * asked — so a caller can neither interrupt somebody else's work nor end a
46
+ * person's sign-in command mid-password. The unkeyed pair that could do those
47
+ * things, `begin_sign_in` and `end_sign_in`, is deliberately not on this
48
+ * surface at all.
49
+ *
50
+ * `browser_tab_close` is absent for a second, separate reason (§3.1): it
51
+ * closed a caller's only tab while keeping the lease, producing a lease that
52
+ * owned nothing and still consumed budget. It is gone rather than deprecated,
53
+ * and it should not be reintroduced.
54
+ */
55
+ /**
56
+ * What a capture can and cannot be trusted to show (§3.11).
57
+ *
58
+ * ── A picture is the one result a caller cannot check against itself ─────
59
+ *
60
+ * Every other operation here returns something self-describing: a claim
61
+ * returns a key, an evaluation returns a value, a refusal explains itself. A
62
+ * capture returns a path to an image, and **an image of a half-rendered page
63
+ * looks exactly like an image of a broken one.** There is no field in the
64
+ * result to distrust, so a caller has nothing to weigh the picture against —
65
+ * which makes a wrong conclusion from a capture both easy to reach and hard
66
+ * to notice.
67
+ *
68
+ * The consequence is not hypothetical, and it is what puts this text on the
69
+ * description rather than in a document: a field session captured a canvas
70
+ * before it had drawn, read the dark frame as the page's real appearance, and
71
+ * **reported a fault against an application that did not have one.** It was
72
+ * caught only because a second measurement happened to disagree — which is
73
+ * not a mechanism anything can rely on.
74
+ *
75
+ * ── Why settling does not already cover this ─────────────────────────────
76
+ *
77
+ * Every capture settles the page first, and settling is worth exactly what
78
+ * §3.11 claims: it stops animations and transitions, hides the caret, and
79
+ * waits for web fonts, so the same page yields the same pixels run to run.
80
+ * **That is repeatability, not completeness.** It makes a moving page hold
81
+ * still; it cannot make an unfinished page finish. A canvas that has not
82
+ * drawn its first frame is not moving — it is absent — and holding it still
83
+ * is not the same as waiting for it.
84
+ *
85
+ * ── Why this names a check rather than a duration ────────────────────────
86
+ *
87
+ * The tempting sentence is *"allow the page to settle first"*, and it is
88
+ * advice a caller cannot act on: **the right wait is a property of the page,
89
+ * not of this service**, and a caller that has never seen the page rendered
90
+ * has no way to pick a number. Any figure written here would be wrong for
91
+ * some page and would be trusted anyway, which is worse than saying nothing.
92
+ *
93
+ * So the guidance is a **check** instead — capture twice and compare, which
94
+ * this tool can already express through `compare_to`. A caller can act on it
95
+ * without knowing anything about the page in advance, and it answers the
96
+ * question actually being asked, which is not *"has enough time passed"* but
97
+ * *"has this stopped changing"*. Two identical frames are evidence; one frame
98
+ * and a duration are an assumption.
99
+ */
100
+ const CAPTURE_SETTLE_CAVEAT = 'Captures are settled — animations stopped, fonts waited for — so a page yields the same ' +
101
+ 'pixels twice; that steadies a moving page but does not wait for one still drawing. A canvas ' +
102
+ 'or a deferred region can be captured before it has rendered, and the picture will look like a ' +
103
+ 'broken page rather than an early one. When a frame looks wrong, capture again with compare_to ' +
104
+ 'and check it against the first: no difference means you are seeing the page, not a moment of it.';
105
+ /**
106
+ * What this tool's unit is wrong for, so a caller with the other job does not
107
+ * reach for it anyway.
108
+ *
109
+ * ── The distinction this exists to draw ──────────────────────────────────
110
+ *
111
+ * `compare_to` answers *"did this page change since a moment ago, on this
112
+ * same tab"* — a before-and-after over time, one lease, one running build.
113
+ * That is a different question from *"how do these two builds differ"*,
114
+ * where the two things being compared are not two moments of one tab but two
115
+ * separate runs, often of separate processes. This surface's unit is one
116
+ * lease holding one tab, which is the right shape for the first question and
117
+ * the wrong one for the second: a two-build comparison wants something that
118
+ * reads the scene or the DOM directly, not pixels from whichever tab happened
119
+ * to be open.
120
+ *
121
+ * ── Why this is worth a sentence rather than leaving it to be inferred ────
122
+ *
123
+ * A caller framing its task as "compare two builds" will reach for whatever
124
+ * on this surface has the word "compare" in it, and `compare_to` is the only
125
+ * candidate. Nothing here refuses that call — a diff still runs and still
126
+ * answers a real question about the two pixels it was given — so the caller
127
+ * gets an answer that looks like the one it asked for while measuring
128
+ * something else. A routing hint at the point of the call is the only thing
129
+ * that can catch this before the wrong tool is already in use; there is no
130
+ * refusal to word better; the call succeeds.
131
+ */
132
+ const CAPTURE_BUILD_COMPARISON_CAVEAT = 'For comparing two builds — not two moments of the one tab you hold — a tool reading the scene ' +
133
+ 'or DOM directly beats this one: this surface is one lease, one tab, pixels.';
3
134
  /** Every tool takes the key except the first and the last (§3.1). */
4
135
  const LEASE_KEY = {
5
136
  name: 'lease_key',
@@ -91,9 +222,10 @@ export const TOOL_DEFINITIONS = [
91
222
  {
92
223
  name: 'browser_navigate',
93
224
  operation: 'navigate',
94
- description: 'Point your tab at an address. Returns the final address after redirects, the title, the ' +
95
- 'status, and a path to the accessibility snapshot taken on arrival a path, because a ' +
96
- 'snapshot is thousands of tokens and you usually want one part of it.',
225
+ description: 'Point your tab at an address. Returns the final address after redirects which is not ' +
226
+ 'always the address you asked for, and is the field to check when you need to know ' +
227
+ 'whether something sent you elsewhere plus the title and the status. ' +
228
+ 'It does NOT take a snapshot: use browser_read for one.',
97
229
  arguments: [
98
230
  LEASE_KEY,
99
231
  {
@@ -107,7 +239,16 @@ export const TOOL_DEFINITIONS = [
107
239
  name: 'wait_ms',
108
240
  type: 'integer',
109
241
  required: false,
110
- description: 'How long to wait for the page, in milliseconds.',
242
+ description: 'How long the navigation may take before it is abandoned, in whole milliseconds. ' +
243
+ 'A bound, not a pause: the call returns as soon as the page is there, so a larger ' +
244
+ 'value costs nothing on a page that loads quickly and two calls differing only in ' +
245
+ 'this argument tell you nothing about how long the page was given to settle. ' +
246
+ 'It bounds the load only, and does not wait for work the page starts afterwards, so ' +
247
+ 'a canvas or a lazily-loaded region can still be unfinished when this returns; see ' +
248
+ 'browser_capture on how to tell. ' +
249
+ 'At most the lease lifetime, because a wait outliving the lease would hold the tab ' +
250
+ 'past the point it becomes reclaimable; the refusal names the accepted range. ' +
251
+ 'Omit it to leave the browser default in force.',
111
252
  },
112
253
  ],
113
254
  },
@@ -175,7 +316,10 @@ export const TOOL_DEFINITIONS = [
175
316
  operation: 'capture',
176
317
  description: 'Take a picture of the page — and, if you name an earlier capture, what changed since it. ' +
177
318
  'Returns paths, never the image itself. A selector and a full page cannot both be asked ' +
178
- 'for. Never refused for cost.',
319
+ 'for. Never refused for cost. ' +
320
+ CAPTURE_SETTLE_CAVEAT +
321
+ ' ' +
322
+ CAPTURE_BUILD_COMPARISON_CAVEAT,
179
323
  arguments: [
180
324
  LEASE_KEY,
181
325
  {
@@ -196,11 +340,23 @@ export const TOOL_DEFINITIONS = [
196
340
  required: false,
197
341
  description: 'An earlier capture to diff against. The diff rides here rather than being its own tool.',
198
342
  },
343
+ {
344
+ name: 'tier',
345
+ type: 'string',
346
+ required: false,
347
+ description: '"detail" or "max" for a higher resolution. Omit for the default — there is no way to ' +
348
+ 'ask for the default by name. "max" also requires reason. **A tier raises the LONGEST ' +
349
+ 'edge, not the width**, so on a full_page capture of a page taller than it is wide the ' +
350
+ 'height takes the whole budget and the width stays small at every rung — a 1030px-wide ' +
351
+ 'page 6400px tall is about 165px wide by default and about 414px wide at "max". For ' +
352
+ 'legible text on a tall page, capture a selector instead, or read the page as text.',
353
+ },
199
354
  {
200
355
  name: 'reason',
201
356
  type: 'string',
202
357
  required: false,
203
- description: 'Free text, recorded, never refused — why this capture needed more than the default tier.',
358
+ description: 'Free text, 8-200 characters, recorded, never refused — why this capture needed more ' +
359
+ 'than the default tier. Required with tier="max".',
204
360
  },
205
361
  ],
206
362
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-broker",
3
- "version": "0.1.0",
3
+ "version": "0.3.1",
4
4
  "license": "MIT",
5
5
  "description": "Leases over tabs in a fixed set of browsers: bounded capacity, a queue, reclamation, and an enforced capture policy.",
6
6
  "type": "module",
@@ -19,12 +19,13 @@
19
19
  "check:operations": "node scripts/check-operations.mjs",
20
20
  "check:argument-refusals": "node scripts/check-argument-refusals.mjs",
21
21
  "check:injected-tests": "node scripts/check-injected-tests.mjs",
22
+ "check:argument-reachability": "node scripts/check-argument-reachability.mjs",
22
23
  "typecheck": "tsc --noEmit",
23
24
  "lint": "eslint .",
24
25
  "format": "prettier --write .",
25
26
  "format:check": "prettier --check .",
26
27
  "test": "node --test \"tests/**/*.test.mjs\" \"tests/**/*.test.ts\"",
27
- "check": "npm run check:external-refs && npm run check:doc-links && npm run check:arbitration && npm run check:capture-isolation && npm run check:artifact-path && npm run typecheck && npm run lint && npm run format:check && npm test && npm run check:install && npm run check:operations && npm run check:argument-refusals && npm run check:injected-tests",
28
+ "check": "npm run check:external-refs && npm run check:doc-links && npm run check:arbitration && npm run check:capture-isolation && npm run check:artifact-path && npm run typecheck && npm run lint && npm run format:check && npm test && npm run check:install && npm run check:operations && npm run check:argument-refusals && npm run check:argument-reachability && npm run check:injected-tests && npm run check:package",
28
29
  "check:arbitration": "node scripts/check-arbitration.mjs",
29
30
  "check:artifact-path": "node scripts/check-artifact-path.mjs",
30
31
  "build": "tsc --project tsconfig.build.json",
@@ -52,7 +53,6 @@
52
53
  "dist/",
53
54
  ".env.example",
54
55
  "README.md",
55
- "RELEASES.md",
56
56
  "LICENSE"
57
57
  ]
58
58
  }
package/RELEASES.md DELETED
@@ -1,97 +0,0 @@
1
- # Release notes
2
-
3
- **What changes between versions, and specifically what changes *behaviour* on an installation that
4
- sets nothing.**
5
-
6
- That second half is the reason this file exists rather than the commit log being enough. A new
7
- setting with a neutral default is not news: an installation that does not set it behaves exactly as
8
- before. **A changed default is news**, because it moves an installation that has taken no action and
9
- made no decision. `docs/plans/DECISIONS.md` §6.3 puts a changed default here rather than in a quiet
10
- edit for that reason.
11
-
12
- Entries are newest first. Each names what moved, what an installation has to do about it, and what
13
- happens if it does nothing.
14
-
15
- ---
16
-
17
- ## Unreleased
18
-
19
- ### The package is published, and it ships compiled JavaScript
20
-
21
- **What moved.** The service is installable from the registry as `browser-broker`, so a caller can
22
- spawn it without a checkout. The manifest's `bin` entries now name emitted JavaScript under `dist/`
23
- rather than the TypeScript sources.
24
-
25
- **Why the build exists**, given that the development path deliberately has none: **Node refuses to
26
- strip types from any file under a `node_modules` path**, and an installed package is a directory
27
- under `node_modules`. A manifest whose `bin` named a `.ts` file would install cleanly and then fail
28
- on the machine of whoever installed it. There is no flag that changes this. The compiler therefore
29
- runs once per release rather than on every machine that consumes the package, and `erasableSyntaxOnly`
30
- stays on so the sources still run unbuilt — the two paths execute the same dialect.
31
-
32
- **What an installation has to do.** Nothing. A checkout is unaffected: `node src/bin/broker.ts` still
33
- runs the sources with no build. An installation that would rather not track a checkout can point at
34
- the package instead, and npm revalidates the version on every spawn:
35
-
36
- ```json
37
- { "command": "npx", "args": ["-y", "browser-broker"] }
38
- ```
39
-
40
- **Worth weighing before switching:** `npx` performs a registry round-trip on every spawn, costing
41
- seconds where a path on disk costs a fraction of one. It buys an upgrade path, not speed.
42
-
43
- **One surface changes what it reports.** The tool handshake's `serverInfo.version` was the literal
44
- `0.0.0` while the package was unversioned, and now reads the manifest — so a client logging it sees
45
- the released version rather than a placeholder.
46
-
47
-
48
- ### ⚠ Behaviour change: the default browser engine is Edge
49
-
50
- **What moved.** A browser launched by this service uses **`msedge`** by default. The previous
51
- behaviour was to launch whatever `chromium.executablePath()` resolved to — the Chromium build the
52
- automation library had fetched.
53
-
54
- **Why.** Edge is present on every Windows machine, so a fresh install runs with nothing set, with no
55
- separate browser download step. That is `DECISIONS.md` §6.1's *"a fresh install runs with nothing
56
- set"* applied to the one prerequisite `npm install` genuinely could not cover.
57
-
58
- **What an installation has to do.** Nothing, if Edge is acceptable. To keep the prior behaviour, or
59
- to pick a different browser, set the engine per kind:
60
-
61
- ```bash
62
- BROKER_REGULAR_BROWSER_ENGINE=chrome # chrome | brave | msedge
63
- BROKER_PRIVATE_BROWSER_ENGINE=chrome # may differ from the line above
64
- ```
65
-
66
- **If it does nothing:** browsers launch under Edge. **Profiles are per browser and are not shared
67
- between engines**, so an installation whose signed-in profile was established under a different
68
- binary will find that browser signed out, and a person will be asked to sign in once more with
69
- `broker login`. Nothing is destroyed — `setup.profile_never_destroyed` still holds, and the earlier
70
- profile directory is left exactly where it is.
71
-
72
- ### Browsers are a configured list, and `browser` on a claim is optional
73
-
74
- **What moved.** Two things, both reversals of recorded decisions — the argument for each is in
75
- `DECISIONS.md` §13i:
76
-
77
- - **The fixed pair of browsers becomes a bounded list per kind**, at most three each, named in
78
- configuration. A name is what a caller claims by and what its profile directory is called.
79
- - **`browser` on `browser_claim` becomes optional.** Unstated resolves to the first signed-in
80
- browser; `regular` or `private` resolves to the first of that kind; a configured name resolves to
81
- that browser exactly.
82
-
83
- **What an installation has to do.** Nothing. The defaults name one browser of each kind, `regular`
84
- and `private`, which is the pair that existed before — so an installation that sets nothing has the
85
- same two browsers under the same two names, and a caller that states `browser` explicitly is
86
- unaffected.
87
-
88
- **Worth reading before configuring more than the default two:** the tab budget counts *tabs*, and
89
- each browser costs a process **before it holds a single tab**. `BROKER_TAB_BUDGET=15` with six
90
- browsers is six browser processes, plus up to fifteen tabs, plus six keeper tabs that are not
91
- counted against the budget at all. `.env.example` states this beside the variables.
92
-
93
- **One schema step.** The store gains a `kind` column on `browsers` and drops the check constraint
94
- that limited a browser's name to two literals. It is applied on the next spawn, like every step, and
95
- the two existing rows are backfilled to their own kinds. **A store stepped by this build is not
96
- readable by an earlier one**, which is the ordinary direction — a build refuses a store newer than
97
- itself rather than downgrading it.