browser-broker 0.1.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.
Files changed (123) hide show
  1. package/.env.example +173 -0
  2. package/LICENSE +21 -0
  3. package/README.md +374 -0
  4. package/RELEASES.md +97 -0
  5. package/dist/package.json +58 -0
  6. package/dist/src/adapter/conformance/case.js +1 -0
  7. package/dist/src/adapter/conformance/cases.js +429 -0
  8. package/dist/src/adapter/conformance/discovery.js +156 -0
  9. package/dist/src/adapter/conformance/driver.js +1 -0
  10. package/dist/src/adapter/conformance/drivers.js +36 -0
  11. package/dist/src/adapter/conformance/run.js +224 -0
  12. package/dist/src/adapter/conformance/service-subject.js +165 -0
  13. package/dist/src/adapter/contract.js +24 -0
  14. package/dist/src/adapter/operations.js +114 -0
  15. package/dist/src/adapter/service-seam.js +1 -0
  16. package/dist/src/artifacts/names.js +229 -0
  17. package/dist/src/artifacts/store.js +174 -0
  18. package/dist/src/bin/broker-tool.js +63 -0
  19. package/dist/src/bin/broker.js +111 -0
  20. package/dist/src/browser/adoption.js +143 -0
  21. package/dist/src/browser/automation-probe.js +113 -0
  22. package/dist/src/browser/conformance/case.js +1 -0
  23. package/dist/src/browser/conformance/cases.js +192 -0
  24. package/dist/src/browser/conformance/run.js +102 -0
  25. package/dist/src/browser/conformance/subjects.js +19 -0
  26. package/dist/src/browser/discovery.js +226 -0
  27. package/dist/src/browser/driver.js +195 -0
  28. package/dist/src/browser/fake.js +585 -0
  29. package/dist/src/browser/launch.js +504 -0
  30. package/dist/src/browser/real.js +1425 -0
  31. package/dist/src/browser/setup.js +161 -0
  32. package/dist/src/capture/accounting.js +59 -0
  33. package/dist/src/capture/image.js +112 -0
  34. package/dist/src/capture/ladder.js +72 -0
  35. package/dist/src/capture/legibility.js +195 -0
  36. package/dist/src/capture/pipeline.js +153 -0
  37. package/dist/src/capture/tiers.js +166 -0
  38. package/dist/src/cli/adapter.js +233 -0
  39. package/dist/src/cli/commands.js +270 -0
  40. package/dist/src/cli/conformance-driver.js +119 -0
  41. package/dist/src/cli/diffs.js +122 -0
  42. package/dist/src/cli/image.js +274 -0
  43. package/dist/src/cli/index.js +895 -0
  44. package/dist/src/cli/login-command.js +401 -0
  45. package/dist/src/cli/operations-commands.js +186 -0
  46. package/dist/src/cli/reconcile-command.js +137 -0
  47. package/dist/src/cli/sign-in.js +134 -0
  48. package/dist/src/cli/telemetry.js +222 -0
  49. package/dist/src/config/environment.js +446 -0
  50. package/dist/src/diff/artifact-path.js +77 -0
  51. package/dist/src/diff/crops.js +102 -0
  52. package/dist/src/diff/geometry.js +122 -0
  53. package/dist/src/diff/image.js +132 -0
  54. package/dist/src/diff/mask.js +46 -0
  55. package/dist/src/diff/regions.js +263 -0
  56. package/dist/src/diff/settings.js +135 -0
  57. package/dist/src/doctor/checks.js +588 -0
  58. package/dist/src/doctor/report.js +152 -0
  59. package/dist/src/doctor/session.js +161 -0
  60. package/dist/src/errors.js +36 -0
  61. package/dist/src/feedback/read.js +119 -0
  62. package/dist/src/feedback/record.js +199 -0
  63. package/dist/src/operations/addresses.js +175 -0
  64. package/dist/src/operations/derive.js +109 -0
  65. package/dist/src/operations/ledger.js +194 -0
  66. package/dist/src/operations/status.js +197 -0
  67. package/dist/src/operations/telemetry.js +280 -0
  68. package/dist/src/report/document.js +419 -0
  69. package/dist/src/report/escape.js +68 -0
  70. package/dist/src/report/snapshot.js +97 -0
  71. package/dist/src/service/arbitration.js +537 -0
  72. package/dist/src/service/artifacts.js +85 -0
  73. package/dist/src/service/bridge.js +577 -0
  74. package/dist/src/service/broker.js +120 -0
  75. package/dist/src/service/browser-session.js +269 -0
  76. package/dist/src/service/capacity.js +62 -0
  77. package/dist/src/service/capture-seam.js +83 -0
  78. package/dist/src/service/capture-store.js +91 -0
  79. package/dist/src/service/comparison-store.js +101 -0
  80. package/dist/src/service/comparison.js +173 -0
  81. package/dist/src/service/events.js +93 -0
  82. package/dist/src/service/keys.js +68 -0
  83. package/dist/src/service/leases.js +147 -0
  84. package/dist/src/service/nudge.js +66 -0
  85. package/dist/src/service/operations/claim.js +692 -0
  86. package/dist/src/service/operations/give-back.js +131 -0
  87. package/dist/src/service/operations/pages.js +771 -0
  88. package/dist/src/service/operations/sign-in.js +915 -0
  89. package/dist/src/service/operations/status.js +62 -0
  90. package/dist/src/service/ownership.js +93 -0
  91. package/dist/src/service/pages.js +616 -0
  92. package/dist/src/service/pending-seeds.js +20 -0
  93. package/dist/src/service/queue.js +233 -0
  94. package/dist/src/service/reconcile.js +220 -0
  95. package/dist/src/service/refusals.js +262 -0
  96. package/dist/src/service/runtime.js +131 -0
  97. package/dist/src/service/signin-recovery.js +148 -0
  98. package/dist/src/service/storage-seed.js +239 -0
  99. package/dist/src/service/tabs.js +123 -0
  100. package/dist/src/store/budget.js +99 -0
  101. package/dist/src/store/location.js +42 -0
  102. package/dist/src/store/network-path.js +182 -0
  103. package/dist/src/store/network-volume.js +92 -0
  104. package/dist/src/store/open.js +226 -0
  105. package/dist/src/store/schema/step-001-initial.js +523 -0
  106. package/dist/src/store/schema/step-002-tab-budget.js +53 -0
  107. package/dist/src/store/schema/step-003-queue-order.js +110 -0
  108. package/dist/src/store/schema/step-004-tab-never-opened.js +100 -0
  109. package/dist/src/store/schema/step-005-storage-seed-event.js +90 -0
  110. package/dist/src/store/schema/step-006-signin-events.js +104 -0
  111. package/dist/src/store/schema/step-007-signin-without-process.js +92 -0
  112. package/dist/src/store/schema/step-008-signin-owner.js +76 -0
  113. package/dist/src/store/schema/step-009-named-browsers.js +138 -0
  114. package/dist/src/store/schema/step-010-signin-request.js +135 -0
  115. package/dist/src/store/schema/step.js +172 -0
  116. package/dist/src/store/schema/steps.js +58 -0
  117. package/dist/src/store/transaction.js +37 -0
  118. package/dist/src/tool/adapter.js +90 -0
  119. package/dist/src/tool/conformance-driver.js +184 -0
  120. package/dist/src/tool/protocol.js +310 -0
  121. package/dist/src/tool/session.js +351 -0
  122. package/dist/src/tool/tools.js +310 -0
  123. package/package.json +58 -0
package/.env.example ADDED
@@ -0,0 +1,173 @@
1
+ # Browser Broker — the configuration registry.
2
+ #
3
+ # This file is DOCUMENTATION. Nothing loads it, and no code path reads a file
4
+ # for configuration: configuration is the process environment, full stop.
5
+ # Copy a line into your own environment if you want to change a value.
6
+ #
7
+ # Every variable has a working default, so a fresh install runs with NOTHING
8
+ # set. A variable that is set and cannot be read as its type refuses the spawn
9
+ # and names the variable — falling back to the default silently would run a
10
+ # configuration nobody chose, with nothing to notice it by.
11
+ #
12
+ # The values below are PLACEHOLDERS showing the shape, never real paths.
13
+ #
14
+ # This file lists the variables the store needs before it can open. The rest
15
+ # of the registry lands with the row that owns it, along with the test that
16
+ # walks the declared variables and asserts this file lists every one.
17
+
18
+ # Where the database file lives.
19
+ # Default: a directory of the service's own under the per-user application-data
20
+ # location the platform defines — which differs per operating system
21
+ # and is computed, never written down.
22
+ # Refused at startup if it resolves to a network location: the write-ahead log
23
+ # coordinates through a shared-memory index that requires every process using
24
+ # the database to be on one host, and a mapped network drive is indistinguishable
25
+ # from a local one by its path alone.
26
+ # BROKER_DB=/path/to/broker.db
27
+
28
+ # Where captures, snapshots and crops are written.
29
+ # Default: as above, in a directory of the service's own.
30
+ # BROKER_ARTIFACTS_ROOT=/path/to/artefacts
31
+
32
+ # Where the browser profiles live. Each browser launches against this plus its
33
+ # own name, explicitly and mandatorily — never a default profile location.
34
+ # Default: as above, in a directory of the service's own.
35
+ # BROKER_PROFILE_ROOT=/path/to/profiles
36
+
37
+ # ── The comparison feature's five numbers ────────────────────────────────
38
+ #
39
+ # A diff is an optional argument on a capture: the caller names which earlier
40
+ # capture to compare against, and with no such argument there is no diff and
41
+ # these are never read. Three of the five are copied onto every comparison
42
+ # record, because all three are mutable and all three decided the output — so
43
+ # what an earlier call did, under the numbers in force at the time, stays
44
+ # answerable after any of them moves.
45
+
46
+ # How different two pixels must be before either counts as changed, from 0 to 1;
47
+ # smaller is more sensitive.
48
+ # Default: 0.1 — the comparison library's own default, which is a better
49
+ # starting position than a number invented here precisely because it is
50
+ # not one.
51
+ # Worth knowing before tuning: at this default a change fainter than roughly
52
+ # grey 225 on white is not reported at all.
53
+ # BROKER_DIFF_COLOUR_TOLERANCE=0.1
54
+
55
+ # The smallest region reported, in SQUARE PIXELS OF AREA.
56
+ # Default: 64
57
+ # Area, with a thin-line allowance — never the shorter side. A filter on the
58
+ # shorter side discards a one-pixel line across a wide page, which is a border,
59
+ # an underline, a focus ring or a rule: the changes most worth catching.
60
+ # BROKER_DIFF_MINIMUM_REGION_AREA=64
61
+
62
+ # How many regions come back before the result is truncated, smallest first.
63
+ # The result says when it was truncated; a shortened list that did not say so
64
+ # would be a lie about completeness.
65
+ # Default: 12
66
+ # BROKER_DIFF_MAXIMUM_REGIONS=12
67
+
68
+ # How far apart two changed areas stay separate regions, in pixels.
69
+ # Default: 8
70
+ # BROKER_DIFF_REGION_MERGE_DISTANCE=8
71
+
72
+ # Context around a crop, in pixels. A tight box with nothing around it can be
73
+ # genuinely unidentifiable.
74
+ # Default: 16
75
+ # BROKER_DIFF_CROP_PADDING=16
76
+
77
+ # The total tab budget across ALL browsers, and — since a lease is a tab —
78
+ # the same number as the maximum count of live leases. No per-browser cap: the
79
+ # scarce thing is page processes and one costs the same in any browser. The
80
+ # keeper tab in each browser is not counted.
81
+ # Provisional: reasoned from roughly 50-150 MB per idle page process plus two
82
+ # browser processes, so fifteen is one to two gigabytes.
83
+ # THIS IS THE ONE VALUE ALSO WRITTEN TO THE STORE. Several processes arbitrate
84
+ # against it at the same moment, so the first process to open the store records
85
+ # the value it believes and every later process compares. On a disagreement the
86
+ # later process refuses to start and names both numbers — neither value is
87
+ # adopted and neither is overwritten.
88
+ # Default: 15
89
+ # BROKER_TAB_BUDGET=15
90
+
91
+ # How long an active lease lives without a call, in seconds.
92
+ # Deliberately NOT given the agreement check the budget gets: two processes
93
+ # disagreeing here expires something early or late, which is degraded behaviour
94
+ # rather than a broken invariant.
95
+ # Default: 600 (ten minutes)
96
+ # BROKER_LEASE_SECONDS=600
97
+
98
+ # How long a place in the queue lives without a call, in seconds.
99
+ # Deliberately EQUAL to the lease lifetime above. Polling is renewing, so a
100
+ # queued caller holds exactly the instrument an active holder does; and under
101
+ # strict first-in-first-out a queue place held longer blocks everyone behind it,
102
+ # so a generous queued lifetime is the harsher setting rather than the kinder one.
103
+ # Default: 600 (ten minutes)
104
+ # BROKER_QUEUE_SECONDS=600
105
+
106
+ # How long a launch-race loser waits for the winner's browser to accept a
107
+ # connection before declaring the launch failed, in seconds.
108
+ # The loser polls the winner's discovery record — the same liveness-and-identity
109
+ # check every attach uses — rather than sleeping for this whole duration; this is
110
+ # only the bound on how long that poll runs before giving up.
111
+ # Default: 30
112
+ # BROKER_LAUNCH_READINESS_TIMEOUT_SECONDS=30
113
+
114
+ # ── The browsers, and what they are called ─────────────────────────
115
+ #
116
+ # Two lists split by kind, rather than one list with a per-entry flag. Which
117
+ # list a name is written in IS its kind, so there is nothing for a name and
118
+ # its kind to disagree about, and nothing needs encoding.
119
+ #
120
+ # A name is what a caller claims by, the name of that browser's profile
121
+ # directory, and its key in the store. Lower-case letters, digits and hyphens,
122
+ # starting with a letter.
123
+ #
124
+ # Each of these refuses the spawn and NAMES THE OFFENDING ENTRY: a duplicate
125
+ # within one list, the same name in both lists, more names than the cap, or a
126
+ # name that is not a usable word.
127
+ #
128
+ # ⚠ READ THIS BEFORE SETTING ALL SIX. The tab budget above does not bound
129
+ # what these cost, because it counts TABS and each browser is a process
130
+ # BEFORE it holds a single tab. So BROKER_TAB_BUDGET=15 with six browsers is
131
+ # six browser processes, plus up to fifteen tabs, plus six keeper tabs that
132
+ # are not counted against the budget at all — six processes and up to
133
+ # twenty-one pages. Reasoned from roughly 50-150 MB per idle page process,
134
+ # that is meaningfully more than the same budget across two browsers, and the
135
+ # budget will not tell you: it is doing its job, and its job is tabs.
136
+
137
+ # The persistent, signed-in browsers, comma-separated, at most 3.
138
+ # THE FIRST ENTRY IS WHAT AN UNSTATED `browser` RESOLVES TO on browser_claim.
139
+ # Each holds a sign-in a person put there by hand, and each is a separate
140
+ # identity — which is the reason for more than one: tabs within a single
141
+ # browser share its cookie jar, so two identities at once means two browsers.
142
+ # Default: regular
143
+ # BROKER_REGULAR_BROWSERS=regular
144
+
145
+ # The ephemeral, signed-in-to-nothing browsers, comma-separated, at most 3.
146
+ # Capped separately rather than sharing a total with the list above, because
147
+ # the two kinds are not interchangeable: one shared total could be spent
148
+ # entirely on signed-in browsers, leaving no clean-room browser at all, and
149
+ # clean-room is the one that cannot be substituted for.
150
+ # Default: private
151
+ # BROKER_PRIVATE_BROWSERS=private
152
+
153
+ # Both caps are refusals, not truncations: naming a fourth browser in either
154
+ # list stops the service at startup and says which variable was wrong, rather
155
+ # than quietly running the first three. `broker doctor` reports on every
156
+ # browser named in both lists above, so a browser added here gains its own
157
+ # discovery and keeper-tab lines in the health report.
158
+
159
+ # Which browser binary the signed-in browsers launch.
160
+ # Options: chrome, brave, msedge — all Chromium over the same remote-debugging
161
+ # protocol, which is why the choice is a path rather than a driver.
162
+ # One engine per kind, never per browser: an engine per entry would put an
163
+ # attribute back on the entry, which is what the two lists exist to avoid.
164
+ # Default: msedge
165
+ # BROKER_REGULAR_BROWSER_ENGINE=msedge
166
+
167
+ # Which browser binary the clean-room browsers launch. MAY DIFFER from the
168
+ # engine above: a person signs into the signed-in browsers by hand, so which
169
+ # binary those are can follow what they already use, while nobody signs into
170
+ # a clean-room browser at all.
171
+ # Options: chrome, brave, msedge
172
+ # Default: msedge
173
+ # BROKER_PRIVATE_BROWSER_ENGINE=msedge
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zaida-3dO
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,374 @@
1
+ # Browser Broker
2
+
3
+ **Several agents, two browsers, no collisions.**
4
+
5
+ Browser Broker brokers access to a small, fixed set of real browsers. A caller asks for a lease; it
6
+ gets back a secret key and **one tab** it exclusively owns. Every later call carries that key, and the
7
+ service uses it to route the call, to enforce what that caller may touch, and to account for what it
8
+ costs. There is nothing running in the background: the service is started by its caller and exits
9
+ with it, and the browsers outlive any one of them.
10
+
11
+ ## What it gives you
12
+
13
+ - **A hard ceiling on browser processes.** Concurrency is expressed in tabs inside a fixed set of
14
+ browsers, so process count is bounded by configuration rather than by how many clients connect. A
15
+ lease is exactly one tab, so the budget, the pool bound and the number of live leases are one
16
+ integer that cannot disagree with itself. Need two tabs, claim twice.
17
+ - **Leases, with a queue.** When capacity is full a caller is queued rather than refused, and told
18
+ its position and when to check back.
19
+ - **Reclamation from callers that die.** Every key carries a time to live that any call renews, so a
20
+ client that vanishes mid-work returns its capacity on its own. This is the failure a client-side
21
+ convention cannot cover, because the client that should clean up is the one that is gone. Nothing
22
+ expires on a timer: every arbitration call first expires whatever has lapsed across the whole
23
+ store, then answers from the reconciled state.
24
+ - **A shared signed-in profile that no single caller can destroy.** Nothing browser-scoped is
25
+ exposed. A caller can close its own tab; it cannot close a browser, and no operation reaches a
26
+ browser outside the ones the service is configured to run.
27
+ - **References, not payloads.** Screenshots and page snapshots are written to disk and returned as a
28
+ path with its dimensions and size. An agent opens one only when it genuinely needs to look, so a
29
+ capture is paid for once instead of on every subsequent turn.
30
+ - **A capture policy applied by the thing that takes the capture.** Screenshots come back at a low
31
+ resolution unless you ask for more, and asking for the most expensive tier costs a stated reason in
32
+ free text. Nothing is ever refused — going over budget warns loudly and names the cheaper way to
33
+ get the same answer.
34
+ - **Changed-region review.** A capture can name an earlier capture to compare against, and the
35
+ regions that actually moved come back as crops — so a repeat review looks at what changed instead
36
+ of at everything. There is no canonical picture to bless first: a capture is a capture with an
37
+ identifier, and the caller says which one it means. If that image is missing, the full screenshot
38
+ comes back with an explanation rather than a refusal.
39
+ - **Nothing to configure before it runs.** Every value is an environment variable with a working
40
+ default, so a fresh install runs with nothing set, and `.env.example` documents the whole set. The
41
+ one value several processes must agree on — the tab budget — is written into the store by the first
42
+ process to open it, and a later process whose environment disagrees refuses to start and names both
43
+ numbers.
44
+
45
+ ## Surfaces
46
+
47
+ The rules live in one service layer, and every surface is a thin adapter over it: **twelve tools**,
48
+ served by the caller's own spawned process, and a `broker` command line that runs the same logic in
49
+ the process you typed it in. A shared conformance suite asserts that the same operation and the same
50
+ refusal happen on both.
51
+
52
+ **Nothing is served over a socket.** The operations view is a self-contained HTML file that a command
53
+ generates and a person opens from disk — a snapshot, labelled with the moment it was taken, which does
54
+ not refresh. It is generated from inside a live session, so it reads each tab's address from the
55
+ browser itself; a browser that does not answer within a timeout renders as unreachable rather than
56
+ hanging the report.
57
+
58
+ ## Install
59
+
60
+ **Installation is the whole of deployment.** There is no image to pull, no daemon to register and no
61
+ service to keep running: the process is started by whatever calls it and exits with it. So getting it
62
+ working is an install and one more fetch below — there is no step after that.
63
+
64
+ You need **Node 22.18 or newer**.
65
+
66
+ ### From the registry
67
+
68
+ The package ships compiled JavaScript, so nothing is built on your machine:
69
+
70
+ ```bash
71
+ npx browser-broker doctor
72
+ ```
73
+
74
+ A client that spawns the tool surface names the same package, and npm revalidates the version on
75
+ every run — so a published release arrives without anything being pulled or rebuilt by hand:
76
+
77
+ ```json
78
+ {
79
+ "mcpServers": {
80
+ "browser-broker": { "command": "npx", "args": ["-y", "browser-broker"] }
81
+ }
82
+ }
83
+ ```
84
+
85
+ Weigh that against spawning a checkout directly: `npx` costs a registry round-trip on every spawn,
86
+ which is several seconds against a fraction of one for a path on disk. A machine that develops this
87
+ service is better off pointing at its own tree; a machine that only *uses* it is better off here.
88
+
89
+ ### From a checkout
90
+
91
+ For working on the service itself. The sources are TypeScript and run through the runtime's own type
92
+ stripping, so there is no build step in the development path:
93
+
94
+ ```bash
95
+ git clone https://github.com/Zaida-3dO/browser-broker.git
96
+ cd browser-broker
97
+ npm install
98
+ ```
99
+
100
+ That compiles the one runtime dependency's native binding, which is *not* the only part of the install
101
+ that does real work: this repository depends on `playwright-core`, not the full `playwright`
102
+ distribution, precisely because the browser binary is spawned by this service, detached and by path,
103
+ rather than downloaded and managed by the package. `playwright-core` does not fetch a browser on
104
+ install, so a checkout that has never had one fetched by some other tooling has none, and `broker
105
+ doctor`'s automation check will genuinely fail with exit code 11 until you run:
106
+
107
+ ```bash
108
+ npx playwright-core install chromium
109
+ ```
110
+
111
+ Run this once per machine, before the first `broker doctor`. It is the same install mechanism the
112
+ full `playwright` package would run automatically on `npm install`; `playwright-core` just does not
113
+ run it for you. Then run the broker itself:
114
+
115
+ ```bash
116
+ node src/bin/broker.ts
117
+ ```
118
+
119
+ The first run creates the store, brings its schema up to the version the build expects, prints where
120
+ the file is, and exits:
121
+
122
+ ```
123
+ store: <the resolved store location>
124
+ schema: stepped from version 0 to version 1 (1 step(s) applied)
125
+ ```
126
+
127
+ Run it again and it says the schema is already where it should be. **Every spawn does this**, not
128
+ just the first — with no long-lived process, there is no other moment at which it could happen, and a
129
+ caller that has upgraded and one that has not may both start within the same minute.
130
+
131
+ To get the command on your path as `broker`, link the package from the checkout:
132
+
133
+ ```bash
134
+ npm link # then: broker --help
135
+ ```
136
+
137
+ ### Configuring it
138
+
139
+ **Nothing needs setting.** Every value is an environment variable with a working default, so the
140
+ install above runs as-is; [`.env.example`](.env.example) documents the whole set, with placeholders
141
+ rather than real values. Nothing reads that file — configuration is the process environment.
142
+
143
+ The default store location is a directory of the service's own under the per-user application-data
144
+ location your platform defines. It is computed rather than written down anywhere, because writing one
145
+ down would name one machine. To put it somewhere else, set `BROKER_DB`:
146
+
147
+ ```bash
148
+ BROKER_DB=/some/writable/path/broker.db node src/bin/broker.ts
149
+ ```
150
+
151
+ **The browsers are configuration too.** Two lists, at most three names each, defaulting to one of
152
+ each — `regular` and `private`. A name is what a caller claims by and what its profile directory is
153
+ called, and a caller that names no browser gets the first signed-in one:
154
+
155
+ ```bash
156
+ BROKER_REGULAR_BROWSERS=regular,checkout # persistent, signed in, at most 3
157
+ BROKER_PRIVATE_BROWSERS=private # ephemeral, at most 3
158
+ BROKER_REGULAR_BROWSER_ENGINE=msedge # chrome | brave | msedge
159
+ BROKER_PRIVATE_BROWSER_ENGINE=msedge # may differ from the line above
160
+ ```
161
+
162
+ Two signed-in browsers is how two identities are exercised at once: tabs within one browser share
163
+ its cookie jar, so they are isolated from other browsers and not from each other. **Note that each
164
+ browser is a process before it holds a single tab**, which the tab budget does not count —
165
+ `.env.example` gives the arithmetic beside the variables.
166
+
167
+ A variable that is set but cannot be read as its type **refuses the spawn and names the variable**,
168
+ rather than quietly falling back to the default — a configuration nobody chose is worse than a
169
+ refusal nobody missed. For a list, the refusal names the offending **entry**: a duplicate within one
170
+ list, a name in both lists, more names than the cap, or a name that is not a usable word. A store location that resolves to a network share is refused for the same
171
+ reason it has to be: the write-ahead log coordinates through shared memory that requires every
172
+ process using the file to sit on one host.
173
+
174
+ ### Pointing a client at the tools
175
+
176
+ The twelve tools are served over standard input and output by `src/bin/broker-tool.ts`, which speaks
177
+ [the Model Context Protocol](https://modelcontextprotocol.io/specification/2025-06-18) — revision
178
+ `2025-06-18`, with `2025-03-26` accepted for a client that asks for it. A client spawns that file,
179
+ opens with `initialize`, and the twelve tools are listed to it.
180
+
181
+ Most clients read a JSON file naming the servers they may spawn. The block is the same shape in all
182
+ of them; put it in whichever file yours reads — commonly `.mcp.json` in a project, or the client's
183
+ own configuration:
184
+
185
+ ```json
186
+ {
187
+ "mcpServers": {
188
+ "browser-broker": {
189
+ "command": "node",
190
+ "args": ["/absolute/path/to/browser-broker/src/bin/broker-tool.ts"]
191
+ }
192
+ }
193
+ }
194
+ ```
195
+
196
+ **The path has to be absolute**, because the client chooses the working directory it spawns from and
197
+ it is rarely the checkout. Nothing else is required: there is no port to configure, no token to
198
+ issue, and no process to have started first — the client starts it, and it exits when the client
199
+ closes the pipe.
200
+
201
+ To point it at a store other than the default, add the environment to the same block:
202
+
203
+ ```json
204
+ {
205
+ "mcpServers": {
206
+ "browser-broker": {
207
+ "command": "node",
208
+ "args": ["/absolute/path/to/browser-broker/src/bin/broker-tool.ts"],
209
+ "env": { "BROKER_DB": "/some/writable/path/broker.db" }
210
+ }
211
+ }
212
+ }
213
+ ```
214
+
215
+ Every client sharing a store sees the same leases, which is the point of the store being a file: the
216
+ capacity being arbitrated is one set of browsers, and two clients that could not see each other's
217
+ claims would both think the whole of it was free.
218
+
219
+ **To check the wiring without a client**, speak the handshake by hand. This writes three messages and
220
+ reads two back — the notification is the one that draws no reply:
221
+
222
+ ```bash
223
+ printf '%s\n' \
224
+ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}' \
225
+ '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
226
+ '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
227
+ | node src/bin/broker-tool.ts
228
+ ```
229
+
230
+ The first response carries the negotiated `protocolVersion`, the server's `capabilities` and its
231
+ `serverInfo`; the second message is a notification and is deliberately not answered; the third lists
232
+ the twelve tools. A client that gets that far will work.
233
+
234
+ ## First run
235
+
236
+ There is **one step a person performs by hand**, and it happens once: signing the shared browser in.
237
+ Everything else — creating the profiles, starting browsers, adopting them, keeping them alive — the
238
+ service does for itself. This is the whole of it, from a clean clone:
239
+
240
+ ```bash
241
+ git clone https://github.com/Zaida-3dO/browser-broker.git
242
+ cd browser-broker
243
+ npm install
244
+
245
+ node src/bin/broker.ts init # create the store and both browser profiles
246
+ node src/bin/broker.ts login # open a browser and sign in, by hand
247
+ node src/bin/broker.ts doctor # confirm the sign-in took
248
+ ```
249
+
250
+ **`broker init`** creates the store, steps its schema, and establishes a profile directory for each
251
+ browser. It reports each one as `created` or `found`, which is the distinction worth reading: a
252
+ profile reported as `created` on a machine where you expected a sign-in is the earliest possible
253
+ warning that you are about to be asked to sign in again. It never recreates or clears a profile that
254
+ is already there.
255
+
256
+ **`broker login`** opens the shared browser, headed, against that profile and hands it to you:
257
+
258
+ ```
259
+ A browser window is open for you to sign in.
260
+
261
+ 1. Switch to the browser window that just opened. It is the regular browser,
262
+ running against the profile at regular under the configured
263
+ profile root — which is the profile every caller will share.
264
+ 2. Go to whichever site you want this service to be signed in to, and sign in
265
+ normally. This is a real browser and a real sign-in: what you type goes to
266
+ that site exactly as it would in your own browser.
267
+ 3. Close the window when you are done. Closing it is what ends this step.
268
+ ```
269
+
270
+ Sign in to whatever you want the service to have access to, then **close the window** — that is what
271
+ ends the step. While it is open the browser is not serving callers: anything that asks for it is told
272
+ a person is signing in and to try again shortly, and anything already waiting in the queue keeps its
273
+ place and its timer. A sign-in is a pause, not a cancellation.
274
+
275
+ Two things it will refuse, both on purpose:
276
+
277
+ - **A browser with live work on it.** If a caller holds a tab there, signing in would mean driving the
278
+ window by hand underneath somebody's work. It names the leases holding it; waiting is enough,
279
+ because every lease expires on its own if its holder stops calling in.
280
+ - **The private browser.** Its profile is discarded when it exits, so a sign-in there would appear to
281
+ work and leave you signed into nothing.
282
+
283
+ **Nothing records what you type.** The sign-in is written into the browser's own profile directory by
284
+ the browser itself. This service never sees a credential and stores nothing about one anywhere —
285
+ which is also why there is no way to copy a sign-in between machines: the profile *is* the identity.
286
+
287
+ **`broker doctor`** then tells you whether it took, without opening a browser:
288
+
289
+ ```
290
+ [ok ] The regular browser’s profile carries a sign-in
291
+ The profile holds 1 stored cookie(s), so a session was established and written down.
292
+ ```
293
+
294
+ Before you have signed in, the same line reads:
295
+
296
+ ```
297
+ [-- ] The regular browser’s profile carries a sign-in
298
+ The profile has a cookie store and it holds no cookies. That is what a profile nobody has
299
+ signed into looks like — though a site that keeps its session only in local storage would
300
+ look the same, so this is the absence of evidence rather than evidence of absence.
301
+ ```
302
+
303
+ **It never reports this as a failure**, and it will say `unknown` rather than guess. A profile with no
304
+ session is the ordinary state of every installation until somebody signs in, and a check that went red
305
+ on a working machine is one people learn to ignore. It also cannot see everything: it reads the
306
+ browser's stored cookies, so a site that keeps its session somewhere else is invisible to it, and a
307
+ browser that is still running has not necessarily written its cookies down yet — in that case it says
308
+ so and tells you to close the browser and ask again.
309
+
310
+ ### Checking an install
311
+
312
+ ```bash
313
+ npm run check:install
314
+ ```
315
+
316
+ This spawns the executable as a real process against a temporary store, and asserts it creates the
317
+ file, steps the schema to the version the build expects, answers a command and **exits**. It is what
318
+ continuous integration runs on a clean hosted runner, and it is the check that stands in for the one
319
+ an image build would have given: proof that the thing actually starts.
320
+
321
+ Run everything the pipeline runs with `npm run check`.
322
+
323
+ ### Recovering from leaked browsers, on Windows
324
+
325
+ <!-- external-ref-ok-next-line: this repository's own first-party PowerShell tool, named for the reader who needs to find it -->
326
+ The emergency sweep in `scripts/reap-broker-browsers.ps1` reports leaked broker browsers by default
327
+ and does not touch them; pass `-Execute` to terminate what it found, and add `-PruneDirs
328
+ -OlderThanHours <n>` to also clear stale profile directories older than that many hours. Not part of
329
+ the pipeline: a browser this project launches is spawned `detached: true` by design (see the header
330
+ of `src/browser/launch.ts`) so it survives the process that started it, and every known way that
331
+ leak could happen has since been fixed at its source — see the script's own header for the list.
332
+ Keep it installed anyway as insurance against whatever the next one turns out to be, and as the tool
333
+ to run if a machine is already in the frozen state a leak like this can cause. Matches processes by
334
+ profile-directory command line, never by image name, so it cannot touch an unrelated Chrome window
335
+ or this repository's own Playwright MCP tooling; see the script's own SAFETY section.
336
+
337
+ ## Status
338
+
339
+ Under construction — the store, the executable and the pipeline are in place, the arbitration
340
+ surface is being built, and the one manual step is wired: `broker login` hands a person the shared
341
+ browser and `broker doctor` reports whether the sign-in took. Read [`docs/ROLLOUT.md`](docs/ROLLOUT.md) for taking it from installed to sole route in an order that
342
+ never leaves traffic unarbitrated, [`docs/plans/PLAN.md`](docs/plans/PLAN.md) for how it works,
343
+ [`docs/plans/DECISIONS.md`](docs/plans/DECISIONS.md) for why it is shaped this way, and
344
+ [`docs/plans/MILESTONES.md`](docs/plans/MILESTONES.md) for the work queue, and
345
+ [`RELEASES.md`](RELEASES.md) for what changes between versions — in particular for defaults that
346
+ move, which change an installation that has taken no action.
347
+
348
+ ## Releasing
349
+
350
+ The development path runs the TypeScript sources directly; the published package cannot, because
351
+ **Node refuses to strip types from any file under a `node_modules` path** and an installed package is
352
+ exactly that. No flag overrides it. So a release compiles to `dist/` and the manifest's `bin` entries
353
+ name the emitted JavaScript.
354
+
355
+ `prepack` runs the build, so `npm publish` and `npm pack` compile on their own — there is no way to
356
+ publish a stale `dist/`, and no build step to remember.
357
+
358
+ ```bash
359
+ npm version patch # or minor / major — writes the tag and the commit
360
+ npm publish # prepack builds, then the tarball goes up
361
+ git push --follow-tags
362
+ ```
363
+
364
+ `npm run check:package` asserts what a published tarball owes: every `bin` target is emitted
365
+ JavaScript rather than a TypeScript source, the whole built tree is included, and the tarball carries
366
+ no tests or plans. It runs in CI. Worth knowing if you touch the `files` field: **npm ships `bin`
367
+ targets whatever `files` says**, so a tarball can contain both executables and none of the modules
368
+ they import — which installs cleanly and dies on first run. That is the case the check exists for.
369
+
370
+ ## Licence
371
+
372
+ **MIT** — see [`LICENSE`](LICENSE), and `docs/plans/DECISIONS.md` §13e for the reasoning. A public
373
+ repository without a licence file grants no rights to anyone, so the decision alone was never enough;
374
+ the file carries it and `package.json` declares it.
package/RELEASES.md ADDED
@@ -0,0 +1,97 @@
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.