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
@@ -0,0 +1,226 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /**
4
+ * Reading the browser's own record of where it can be reached, and checking
5
+ * it before anybody attaches on the strength of it.
6
+ *
7
+ * ── Why the browser picks the port and not this service ─────────────────
8
+ *
9
+ * `SCHEMA.md` §1.2c: the browser is asked to listen on an **unspecified**
10
+ * port, so the operating system assigns a free one, and the browser writes
11
+ * the result into a file inside its own profile directory. Two properties
12
+ * follow, and both are the reason for the arrangement:
13
+ *
14
+ * - **No collision with anything else on the machine.** A fixed port is a
15
+ * guess about what else is running, and the inward-isolation rule says this
16
+ * service must start correctly on a host where unrelated things are already
17
+ * listening.
18
+ * - **The record cannot drift from the identity it describes**, because it
19
+ * lives inside the profile directory that *is* the identity (§1.2). A
20
+ * record kept anywhere else is a second place the truth lives, and the two
21
+ * go out of step the first time something exits badly.
22
+ *
23
+ * ── The property this module exists for: a claim, not a proof ───────────
24
+ *
25
+ * The record **survives the browser**. This was verified rather than assumed,
26
+ * and re-verified while building this row: after the process was killed
27
+ * outright the file was still present, still readable, and still naming a
28
+ * port that answered nothing.
29
+ *
30
+ * So {@link readDiscoveryRecord} returning a record means *a file said so*
31
+ * and nothing more. {@link verifyDiscoveryRecord} is what turns it into a
32
+ * fact, and it owes **two** checks rather than one:
33
+ *
34
+ * 1. **Liveness.** The endpoint answers. A file is not a process.
35
+ * 2. **Identity.** The browser's own identifier matches. **Ports are
36
+ * reused** — a stale record plus an unrelated process that happened to be
37
+ * given the same port reads as a successful match against the number
38
+ * alone, and the service would attach to something it has no business
39
+ * touching.
40
+ *
41
+ * A record that fails either check is stale: the browser is treated as not
42
+ * running, and whichever caller notices takes the launch race (§1.2a).
43
+ */
44
+ /**
45
+ * The file the browser writes inside its profile directory.
46
+ *
47
+ * Named by the browser rather than chosen here, which is why it is a constant
48
+ * and not configuration: this service reads a file somebody else's code
49
+ * writes, so the name is a fact to be matched, not a preference to be set.
50
+ */
51
+ export const PORT_FILE_NAME = 'DevToolsActivePort';
52
+ /** Where a profile directory lives, given the configured root and a browser. */
53
+ export function profileDirectory(profileRoot, browser) {
54
+ // The directory is the root plus the browser's own id, computed rather than
55
+ // stored: `SCHEMA.md` §1.2 keeps no `profile_dir` column, because storing
56
+ // one stores an absolute path the database already knows how to compute,
57
+ // and §1.7a's rule is that no absolute path is ever stored anywhere.
58
+ return path.join(profileRoot, browser);
59
+ }
60
+ /** The record file's path for a profile directory. */
61
+ export function portFilePath(profileDir) {
62
+ return path.join(profileDir, PORT_FILE_NAME);
63
+ }
64
+ /**
65
+ * Parse the record file's contents.
66
+ *
67
+ * Returns `undefined` for anything that is not a well-formed record rather
68
+ * than throwing, because an unreadable or half-written record is *the browser
69
+ * is not reachable this way* — the same conclusion as an absent one — and a
70
+ * caller that has to tell a malformed file from a missing one in order to
71
+ * decide whether to launch has been handed a distinction that changes
72
+ * nothing. A truncated file is a real state: the browser writes it in two
73
+ * lines and a reader can arrive between them.
74
+ */
75
+ export function parsePortFile(contents) {
76
+ const [portLine, wsPath] = contents.split('\n');
77
+ if (portLine === undefined || wsPath === undefined) {
78
+ return undefined;
79
+ }
80
+ const port = Number(portLine.trim());
81
+ // An empty string converts to zero and a partly-written line converts to a
82
+ // non-number, so both cases have to be excluded explicitly rather than by
83
+ // truthiness — zero is exactly what an unfinished write looks like.
84
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
85
+ return undefined;
86
+ }
87
+ // The identifier is the last segment of the per-browser debugging path.
88
+ const browserUuid = wsPath.trim().split('/').at(-1);
89
+ if (browserUuid === undefined || browserUuid === '') {
90
+ return undefined;
91
+ }
92
+ return { port, browserUuid };
93
+ }
94
+ /**
95
+ * Read the record a browser left in its profile directory.
96
+ *
97
+ * **The returned record deliberately carries no `browserUuid`.** The shape on
98
+ * the seam makes that field optional and says why: it is absent on a record
99
+ * that has been read off disk but not yet checked against a live browser,
100
+ * which is the ordinary state of one. The identifier read from the *file* is
101
+ * the browser's claim about itself; the identifier that matters is the one a
102
+ * live endpoint reports. Keeping the file's copy out of the record is what
103
+ * stops a later reader treating a value that came off disk as though it had
104
+ * been confirmed.
105
+ *
106
+ * The file's identifier is not discarded — {@link verifyDiscoveryRecord}
107
+ * takes it as the expectation to match against. It simply does not travel
108
+ * inside a type whose presence means *verified*.
109
+ */
110
+ export function readDiscoveryRecord(profileDir) {
111
+ let contents;
112
+ try {
113
+ contents = fs.readFileSync(portFilePath(profileDir), 'utf8');
114
+ }
115
+ catch {
116
+ // Absent, unreadable, or a directory. Every one of them means the same
117
+ // thing to a caller: there is no record to attach on.
118
+ return undefined;
119
+ }
120
+ const parsed = parsePortFile(contents);
121
+ if (parsed === undefined) {
122
+ return undefined;
123
+ }
124
+ return {
125
+ record: { endpoint: `http://127.0.0.1:${String(parsed.port)}` },
126
+ expectedUuid: parsed.browserUuid,
127
+ };
128
+ }
129
+ /**
130
+ * How long to wait for the endpoint to answer before calling it unreachable.
131
+ *
132
+ * This bounds **one request to a loopback address**, which either answers
133
+ * immediately or is not there — it is not the launch-readiness bound, which
134
+ * is row #55's open question (§1.2b) and is a different quantity entirely. A
135
+ * bound is needed at all because a port can be held by something that accepts
136
+ * a connection and never replies, and a verification step that inherits that
137
+ * hang stalls every caller instead of concluding the browser is not usable.
138
+ */
139
+ export const ENDPOINT_TIMEOUT_MS = 2000;
140
+ /**
141
+ * Check that the endpoint answers **and** that the browser behind it is the
142
+ * one the record described.
143
+ *
144
+ * Both checks, never one. Liveness alone attaches to whatever inherited the
145
+ * port; identity alone cannot be read without something answering.
146
+ */
147
+ export async function verifyDiscoveryRecord(record, expectedUuid, options = {}) {
148
+ const fetchImpl = options.fetchImpl ?? fetch;
149
+ const timeoutMs = options.timeoutMs ?? ENDPOINT_TIMEOUT_MS;
150
+ // ── Why the bound is a ref'd timer and NOT `AbortSignal.timeout` ───────
151
+ //
152
+ // `AbortSignal.timeout` is the obvious way to bound a request, it reads as
153
+ // correct, and it **is** correct whenever something else is keeping the
154
+ // process alive — which is every test run. Its timer is **unref'd**, so it
155
+ // cannot on its own keep the event loop running.
156
+ //
157
+ // That is fatal here specifically. This service is a **short-lived spawn**
158
+ // (§1.0a): started by a caller, serves that session, exits with it. On the
159
+ // adopt path the whole design rests on, a caller that is only verifying a
160
+ // record has **no other outstanding ref'd work** — and against a port that
161
+ // accepts a connection and never replies, the only thing that could settle
162
+ // the request is a timer incapable of holding the loop open. The process
163
+ // therefore **exits without concluding**, silently and with a success code,
164
+ // rather than reporting `endpoint_unreachable`.
165
+ //
166
+ // Measured on the runtime the pipeline uses: with nothing else outstanding
167
+ // the abort never fires and the process exits; adding one unrelated ref'd
168
+ // timer to the byte-identical call makes it conclude correctly. That
169
+ // contrast is the mechanism — the bound was being enforced by luck.
170
+ //
171
+ // So the timer is **ref'd** (an ordinary `setTimeout`, which keeps the loop
172
+ // alive until it fires) and is **cleared in a `finally`**. Both halves are
173
+ // required: ref'd without clearing would trade a silent early exit for a
174
+ // guaranteed delay on every call that answered promptly.
175
+ const controller = new AbortController();
176
+ const bound = setTimeout(() => {
177
+ controller.abort(new Error(`the endpoint did not answer within ${String(timeoutMs)}ms`));
178
+ }, timeoutMs);
179
+ let payload;
180
+ try {
181
+ const response = await fetchImpl(`${record.endpoint}/json/version`, {
182
+ signal: controller.signal,
183
+ });
184
+ if (!response.ok) {
185
+ return {
186
+ ok: false,
187
+ failure: 'endpoint_unreachable',
188
+ detail: `The endpoint answered with status ${String(response.status)} rather than a version document.`,
189
+ };
190
+ }
191
+ payload = (await response.json());
192
+ }
193
+ catch (error) {
194
+ return {
195
+ ok: false,
196
+ failure: 'endpoint_unreachable',
197
+ detail: `The endpoint did not answer: ${error instanceof Error ? error.message : String(error)}. The record is a claim, not a proof — it survives the browser it describes.`,
198
+ };
199
+ }
200
+ finally {
201
+ // Cleared on every path out of the request, including the early returns
202
+ // above: a bound that outlived the thing it bounds would hold the process
203
+ // open for the rest of its window on every successful call.
204
+ clearTimeout(bound);
205
+ }
206
+ const url = payload.webSocketDebuggerUrl;
207
+ const actualUuid = typeof url === 'string' ? url.split('/').at(-1) : undefined;
208
+ if (actualUuid === undefined || actualUuid === '') {
209
+ return {
210
+ ok: false,
211
+ failure: 'identity_mismatch',
212
+ detail: 'The endpoint answered but reported no browser identifier, so there is nothing to match the record against.',
213
+ };
214
+ }
215
+ if (actualUuid !== expectedUuid) {
216
+ // The case this exists for: the recorded browser exited and an unrelated
217
+ // process was handed the same port. Matching the number alone would read
218
+ // as success and attach to a stranger.
219
+ return {
220
+ ok: false,
221
+ failure: 'identity_mismatch',
222
+ detail: 'The endpoint answered, but it is a different browser from the one the record describes. Ports are reused, so a matching port is not a matching browser.',
223
+ };
224
+ }
225
+ return { ok: true, record: { endpoint: record.endpoint, browserUuid: actualUuid } };
226
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * The seam between the service and whatever actually drives a browser.
3
+ *
4
+ * Everything in this file is types and constants. There is no automation
5
+ * library behind it yet and no runtime dependency added for it — row #20
6
+ * brings the real implementation, and rows #21 through #24, #53, #54 and #56
7
+ * fill in the operations declared here. What lands now is the shape those
8
+ * rows plug into, and a fake that satisfies it (`fake.ts`).
9
+ *
10
+ * ── Why the interface is worth its own row ──────────────────────────────
11
+ *
12
+ * `DECISIONS.md` §5: *a guard that returns "denied" after the tab already
13
+ * opened is worse than no guard*, because it reports a refusal that did not
14
+ * happen and everything downstream believes it. A rejection test that only
15
+ * asserts the response cannot tell those two apart. So the seam exists to be
16
+ * faked, and the fake exists to make *nothing happened* an assertable claim.
17
+ * That is why this row comes before the real driver rather than after it.
18
+ *
19
+ * ── What this shape enforces, and what it merely encourages ─────────────
20
+ *
21
+ * Worth stating exactly, in the manner `transaction.ts` states its own
22
+ * bypasses, because the difference is where a later row will get hurt:
23
+ *
24
+ * **Structural — the compiler refuses these.**
25
+ *
26
+ * - **A browser's *kind* is one of two, and there is no third.**
27
+ * {@link BrowserKind} is a union of two string literals, and the store
28
+ * enforces the same thing with a check constraint (schema step nine), so
29
+ * the code that talks to browsers agrees with the database by
30
+ * construction.
31
+ *
32
+ * **What is not structural, said plainly rather than left to be
33
+ * discovered:** the *number* of browsers. `DECISIONS.md` §13i makes the
34
+ * configured browsers a bounded list per kind read from the environment,
35
+ * and a set that is not known until a spawn starts cannot be a type. The
36
+ * bound is enforced — by
37
+ * `src/config/environment.ts`, which refuses a list over its cap and names
38
+ * the entries — but **it is enforced at startup rather than by the
39
+ * compiler**, and a reader who assumes otherwise will look for a guarantee
40
+ * in the wrong place.
41
+ * - **No operation takes a list of tabs.** Every member of
42
+ * {@link TabOperations} is singular, because a lease is one tab (§2.3) and
43
+ * `SCHEMA.md` §3.1 puts it plainly: there was never more than one to list.
44
+ * There is no plural close and no "close all my tabs" to write.
45
+ * - **A tab is closed through the handle for that tab.** {@link TabHandle} is
46
+ * what {@link BrowserSession.openTab} returns, so closing one requires
47
+ * already holding it.
48
+ *
49
+ * **Conventional — this shape makes the wrong thing awkward and nothing
50
+ * more.** Say so rather than implying otherwise:
51
+ *
52
+ * - **`browser_scoped.never` is not enforced here.** No method on
53
+ * {@link BrowserSession} closes, kills, restarts or reaps a browser, so
54
+ * there is no browser-scoped destructive call to reach for and adding one
55
+ * would be a visible new method on a documented seam rather than an
56
+ * argument slipped into an existing call. That is deliberately as far as it
57
+ * goes: an implementation of this interface holds a live connection to a
58
+ * browser, and nothing in a type signature can stop the module that holds
59
+ * it from doing something else with it. `SCHEMA.md` §7.3 makes
60
+ * `browser_scoped.never` a **build rule** for exactly that reason, and the
61
+ * rule is checked against the agent surface, which this is not. Do not read
62
+ * the absence of a close-browser method as the guarantee; it is the easy
63
+ * path pointing the right way.
64
+ * - **Adoption is a protocol, not a constructor.** {@link BrowserDriver}
65
+ * offers {@link BrowserDriver.attach} and {@link BrowserDriver.coldStart}
66
+ * as separate calls because they are separate acts with different risks
67
+ * (§1.2a), and nothing here forces a caller to try the first before the
68
+ * second. Row #54 arbitrates which one a given caller performs, in the
69
+ * store, in the same transaction that arbitrates claims. **The ordering
70
+ * rule lives there, not in this file.**
71
+ * - **Nothing here keeps browser work out of the arbitration transaction.**
72
+ * Every operation is `async`, and `SCHEMA.md` §2.4b's rule is that none of
73
+ * them is reachable from inside a transaction. What makes that the path of
74
+ * least resistance is `transaction.ts`'s `afterCommit`, which already takes
75
+ * the collect-inside-act-outside shape; what makes it checkable is the
76
+ * `arbitration.no_browser_io` build rule (§7.3). This file cannot do either
77
+ * job, and a comment claiming it did would be the kind of false assurance
78
+ * §7.3 exists to replace.
79
+ */
80
+ /** Both kinds, in a fixed order, for anything that has to visit each. */
81
+ export const BROWSER_KINDS = ['regular', 'private'];
82
+ /**
83
+ * The browsers the default configuration names, in a fixed order.
84
+ *
85
+ * **The default set, never the permitted set.** Anything deciding what a
86
+ * particular installation actually has must read the configured lists or the
87
+ * store; this is what an installation that has set nothing runs, and it is
88
+ * what fixtures and the fake driver stand up when no configuration is in
89
+ * play. Reading it as the permitted set is the mistake this sentence exists
90
+ * to prevent.
91
+ */
92
+ export const DEFAULT_BROWSER_IDS = ['regular', 'private'];
93
+ /**
94
+ * **Which browser to claim, in one sentence** (`SCHEMA.md` §1.2, §3.2, row
95
+ * #66).
96
+ *
97
+ * ── Why this string exists, rather than the guidance being written twice ──
98
+ *
99
+ * §3.2 requires the same guidance in two places: **the tool's description
100
+ * text**, which is the only surface a calling agent reliably reads, and **the
101
+ * claim refusal**, because a caller re-reading a refusal is a caller
102
+ * re-making this decision. Two hand-written copies drift, and the copy that
103
+ * goes stale is the one nobody is looking at.
104
+ *
105
+ * ── Why the guidance is needed at all ───────────────────────────────────
106
+ *
107
+ * **Measured: 25 sessions seeded authentication into an isolated browser
108
+ * while the signed-in browser sat unused** — doing by hand, unreliably, the
109
+ * one thing that browser exists to provide. Nothing in this design assigns a
110
+ * kind of caller to a browser; it is a per-claim choice, and nothing told
111
+ * callers how to make it. **A capability nobody finds is worth what an absent
112
+ * capability is worth.**
113
+ *
114
+ * ── The caveat travels with it, and that is not optional ────────────────
115
+ *
116
+ * **Tabs in one browser share its cookie jar.** Two callers in the same
117
+ * browser are clean-room relative to every other browser and **relative to
118
+ * nothing else** — not to each other. That is right for reviewing an
119
+ * authenticated surface, where every caller wants the same identity and wants
120
+ * it to be the real one, and **wrong for exercising two identities at once in
121
+ * one browser**.
122
+ *
123
+ * Two identities at once is what a second configured browser is for
124
+ * (`DECISIONS.md` §13i), and the guidance says so rather than leaving it to
125
+ * be discovered from a test that mysteriously sees the wrong account.
126
+ *
127
+ * Stating the caveat in the same breath is the whole point: guidance that
128
+ * sent callers to the signed-in browser without it would trade one silent
129
+ * failure for another.
130
+ *
131
+ * ── The default makes this string carry more, not less ──────────────────
132
+ *
133
+ * `browser` is optional, and an unstated one resolves to the first signed-in
134
+ * browser (§3.2, `DECISIONS.md` §13i). **That is an argument for this text
135
+ * being more load-bearing rather than less**, and it is worth saying because
136
+ * the opposite reading is the tempting one. A caller that states nothing
137
+ * never sees the unknown-browser refusal, so the refusal stops being a place
138
+ * the caveat can reach it — and the default routes *more* traffic into one
139
+ * shared cookie jar, which is the exact condition the caveat is about. This
140
+ * is where a caller learns it or does not learn it at all.
141
+ *
142
+ * ── Kept short on purpose ───────────────────────────────────────────────
143
+ *
144
+ * §3.1: **surface area is a standing tax.** This text sits in a connected
145
+ * session's context on every turn, so it is the shortest wording that still
146
+ * carries the choice and its caveat. It is not an essay and must not become
147
+ * one.
148
+ */
149
+ export const BROWSER_CHOICE_GUIDANCE = 'Omit this and you get the first signed-in browser, which is what most work wants. ' +
150
+ 'Pass "private" for genuinely-fresh-visitor work — first-visit behaviour, an ' +
151
+ 'undismissed banner, a consent prompt as a stranger sees it — or a configured ' +
152
+ 'browser name to pick that one exactly. Tabs in one browser share its cookie jar, ' +
153
+ 'so callers in the same browser are isolated from other browsers but not from each ' +
154
+ 'other: for two identities at once, claim two differently-named browsers rather than ' +
155
+ 'two tabs in one.';
156
+ /** Every action, in the order §3.8 lists them, for a refusal that names them all. */
157
+ export const PAGE_ACTIONS = [
158
+ 'click',
159
+ 'type',
160
+ 'fill',
161
+ 'press',
162
+ 'select',
163
+ 'hover',
164
+ 'check',
165
+ 'scroll',
166
+ 'resize',
167
+ 'emulate',
168
+ 'dialog',
169
+ 'fill_form',
170
+ 'drag',
171
+ ];
172
+ /**
173
+ * Every artefact a read can ask for, for a caller that has to enumerate them.
174
+ *
175
+ * Snapshot first because it is the default and the only load-bearing one
176
+ * (`SCHEMA.md` §3.9): every element reference `browser_act` takes comes from
177
+ * it, so a read that omitted it would be useless in the ordinary case.
178
+ */
179
+ export const READ_ARTIFACTS = [
180
+ 'snapshot',
181
+ 'console',
182
+ 'network',
183
+ 'cookies',
184
+ ];
185
+ /**
186
+ * Which artefacts are which. Written down so *"is this already being
187
+ * collected"* has a stable answer per artefact rather than being something to
188
+ * reason out each time somebody reads the default.
189
+ */
190
+ export const ARTIFACT_COLLECTION = {
191
+ snapshot: 'generated',
192
+ console: 'accumulated',
193
+ network: 'accumulated',
194
+ cookies: 'live',
195
+ };