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.
- package/.env.example +173 -0
- package/LICENSE +21 -0
- package/README.md +374 -0
- package/RELEASES.md +97 -0
- package/dist/package.json +58 -0
- package/dist/src/adapter/conformance/case.js +1 -0
- package/dist/src/adapter/conformance/cases.js +429 -0
- package/dist/src/adapter/conformance/discovery.js +156 -0
- package/dist/src/adapter/conformance/driver.js +1 -0
- package/dist/src/adapter/conformance/drivers.js +36 -0
- package/dist/src/adapter/conformance/run.js +224 -0
- package/dist/src/adapter/conformance/service-subject.js +165 -0
- package/dist/src/adapter/contract.js +24 -0
- package/dist/src/adapter/operations.js +114 -0
- package/dist/src/adapter/service-seam.js +1 -0
- package/dist/src/artifacts/names.js +229 -0
- package/dist/src/artifacts/store.js +174 -0
- package/dist/src/bin/broker-tool.js +63 -0
- package/dist/src/bin/broker.js +111 -0
- package/dist/src/browser/adoption.js +143 -0
- package/dist/src/browser/automation-probe.js +113 -0
- package/dist/src/browser/conformance/case.js +1 -0
- package/dist/src/browser/conformance/cases.js +192 -0
- package/dist/src/browser/conformance/run.js +102 -0
- package/dist/src/browser/conformance/subjects.js +19 -0
- package/dist/src/browser/discovery.js +226 -0
- package/dist/src/browser/driver.js +195 -0
- package/dist/src/browser/fake.js +585 -0
- package/dist/src/browser/launch.js +504 -0
- package/dist/src/browser/real.js +1425 -0
- package/dist/src/browser/setup.js +161 -0
- package/dist/src/capture/accounting.js +59 -0
- package/dist/src/capture/image.js +112 -0
- package/dist/src/capture/ladder.js +72 -0
- package/dist/src/capture/legibility.js +195 -0
- package/dist/src/capture/pipeline.js +153 -0
- package/dist/src/capture/tiers.js +166 -0
- package/dist/src/cli/adapter.js +233 -0
- package/dist/src/cli/commands.js +270 -0
- package/dist/src/cli/conformance-driver.js +119 -0
- package/dist/src/cli/diffs.js +122 -0
- package/dist/src/cli/image.js +274 -0
- package/dist/src/cli/index.js +895 -0
- package/dist/src/cli/login-command.js +401 -0
- package/dist/src/cli/operations-commands.js +186 -0
- package/dist/src/cli/reconcile-command.js +137 -0
- package/dist/src/cli/sign-in.js +134 -0
- package/dist/src/cli/telemetry.js +222 -0
- package/dist/src/config/environment.js +446 -0
- package/dist/src/diff/artifact-path.js +77 -0
- package/dist/src/diff/crops.js +102 -0
- package/dist/src/diff/geometry.js +122 -0
- package/dist/src/diff/image.js +132 -0
- package/dist/src/diff/mask.js +46 -0
- package/dist/src/diff/regions.js +263 -0
- package/dist/src/diff/settings.js +135 -0
- package/dist/src/doctor/checks.js +588 -0
- package/dist/src/doctor/report.js +152 -0
- package/dist/src/doctor/session.js +161 -0
- package/dist/src/errors.js +36 -0
- package/dist/src/feedback/read.js +119 -0
- package/dist/src/feedback/record.js +199 -0
- package/dist/src/operations/addresses.js +175 -0
- package/dist/src/operations/derive.js +109 -0
- package/dist/src/operations/ledger.js +194 -0
- package/dist/src/operations/status.js +197 -0
- package/dist/src/operations/telemetry.js +280 -0
- package/dist/src/report/document.js +419 -0
- package/dist/src/report/escape.js +68 -0
- package/dist/src/report/snapshot.js +97 -0
- package/dist/src/service/arbitration.js +537 -0
- package/dist/src/service/artifacts.js +85 -0
- package/dist/src/service/bridge.js +577 -0
- package/dist/src/service/broker.js +120 -0
- package/dist/src/service/browser-session.js +269 -0
- package/dist/src/service/capacity.js +62 -0
- package/dist/src/service/capture-seam.js +83 -0
- package/dist/src/service/capture-store.js +91 -0
- package/dist/src/service/comparison-store.js +101 -0
- package/dist/src/service/comparison.js +173 -0
- package/dist/src/service/events.js +93 -0
- package/dist/src/service/keys.js +68 -0
- package/dist/src/service/leases.js +147 -0
- package/dist/src/service/nudge.js +66 -0
- package/dist/src/service/operations/claim.js +692 -0
- package/dist/src/service/operations/give-back.js +131 -0
- package/dist/src/service/operations/pages.js +771 -0
- package/dist/src/service/operations/sign-in.js +915 -0
- package/dist/src/service/operations/status.js +62 -0
- package/dist/src/service/ownership.js +93 -0
- package/dist/src/service/pages.js +616 -0
- package/dist/src/service/pending-seeds.js +20 -0
- package/dist/src/service/queue.js +233 -0
- package/dist/src/service/reconcile.js +220 -0
- package/dist/src/service/refusals.js +262 -0
- package/dist/src/service/runtime.js +131 -0
- package/dist/src/service/signin-recovery.js +148 -0
- package/dist/src/service/storage-seed.js +239 -0
- package/dist/src/service/tabs.js +123 -0
- package/dist/src/store/budget.js +99 -0
- package/dist/src/store/location.js +42 -0
- package/dist/src/store/network-path.js +182 -0
- package/dist/src/store/network-volume.js +92 -0
- package/dist/src/store/open.js +226 -0
- package/dist/src/store/schema/step-001-initial.js +523 -0
- package/dist/src/store/schema/step-002-tab-budget.js +53 -0
- package/dist/src/store/schema/step-003-queue-order.js +110 -0
- package/dist/src/store/schema/step-004-tab-never-opened.js +100 -0
- package/dist/src/store/schema/step-005-storage-seed-event.js +90 -0
- package/dist/src/store/schema/step-006-signin-events.js +104 -0
- package/dist/src/store/schema/step-007-signin-without-process.js +92 -0
- package/dist/src/store/schema/step-008-signin-owner.js +76 -0
- package/dist/src/store/schema/step-009-named-browsers.js +138 -0
- package/dist/src/store/schema/step-010-signin-request.js +135 -0
- package/dist/src/store/schema/step.js +172 -0
- package/dist/src/store/schema/steps.js +58 -0
- package/dist/src/store/transaction.js +37 -0
- package/dist/src/tool/adapter.js +90 -0
- package/dist/src/tool/conformance-driver.js +184 -0
- package/dist/src/tool/protocol.js +310 -0
- package/dist/src/tool/session.js +351 -0
- package/dist/src/tool/tools.js +310 -0
- package/package.json +58 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step four: a tab that ended without ever opening becomes representable.
|
|
3
|
+
*
|
|
4
|
+
* **A new step rather than an edit to step one**, per the rule that a step
|
|
5
|
+
* which has run somewhere is history (`CLAUDE.md`, `SCHEMA.md` §1.2d).
|
|
6
|
+
* SQLite cannot alter a check in place, so the table is rebuilt — which is
|
|
7
|
+
* why this step is longer than the change it makes.
|
|
8
|
+
*
|
|
9
|
+
* ── The defect, and why the constraint rather than the writer is wrong ───
|
|
10
|
+
*
|
|
11
|
+
* Step one carries `CHECK ((state = 'opening') = (driver_tab_id IS NULL))`,
|
|
12
|
+
* an **equivalence**: a null driver name is permitted on `opening` and
|
|
13
|
+
* nowhere else. Its own comment gives the reason it exists, and the reason is
|
|
14
|
+
* narrower than the rule it wrote:
|
|
15
|
+
*
|
|
16
|
+
* > A tab that has not opened has no driver name to be unique against, and
|
|
17
|
+
* > one that has opened does. **Without this the partial unique index below
|
|
18
|
+
* > would be satisfied by any number of live rows holding null.**
|
|
19
|
+
*
|
|
20
|
+
* That index is `one_row_per_physical_tab`, and it covers
|
|
21
|
+
* `state IN ('opening', 'open', 'closing')`. **`closed` and `failed` are
|
|
22
|
+
* outside it**, so a null driver name on a closed row cannot dilute a
|
|
23
|
+
* uniqueness rule that does not range over it. The equivalence constrains two
|
|
24
|
+
* states its own justification never reaches.
|
|
25
|
+
*
|
|
26
|
+
* ── What that cost, measured ────────────────────────────────────────────
|
|
27
|
+
*
|
|
28
|
+
* A lease ends in one of two ways and both have to write the tab row.
|
|
29
|
+
* A tab that never opened has no driver name, so **every** terminal state was
|
|
30
|
+
* refused for it: `closing` is dishonest (§1.4 — nothing was asked and no
|
|
31
|
+
* page may still exist), and `closed` was forbidden by the equivalence. The
|
|
32
|
+
* row could not leave `opening`, so the update threw.
|
|
33
|
+
*
|
|
34
|
+
* Because the sweep runs unconditionally before every handler (§1.0a, and
|
|
35
|
+
* that is correct), **one lapsed lease holding such a tab made every
|
|
36
|
+
* arbitration call by every caller throw, permanently and across spawns** —
|
|
37
|
+
* the lapsed row could never be swept, because sweeping it was the operation
|
|
38
|
+
* that threw. A lapsing lease is the ordinary case the lazy sweep exists to
|
|
39
|
+
* serve (§2.4), so this was the main line rather than an edge.
|
|
40
|
+
*
|
|
41
|
+
* ── The narrowing, stated exactly ───────────────────────────────────────
|
|
42
|
+
*
|
|
43
|
+
* | State | Driver name | Before | Now |
|
|
44
|
+
* |---|---|---|---|
|
|
45
|
+
* | `opening` | null | required | **required**, unchanged |
|
|
46
|
+
* | `open` · `closing` | present | required | **required**, unchanged |
|
|
47
|
+
* | `closed` · `failed` | either | null refused | **permitted** |
|
|
48
|
+
*
|
|
49
|
+
* The live states keep the rule exactly as it was, which is the whole of what
|
|
50
|
+
* the index needs. What becomes representable is the true fact the design had
|
|
51
|
+
* no way to record: **a tab whose lease ended before the tab ever opened.**
|
|
52
|
+
* Nothing is loosened while a tab is live.
|
|
53
|
+
*/
|
|
54
|
+
const REBUILD = [
|
|
55
|
+
`
|
|
56
|
+
CREATE TABLE tabs_next (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
claim_id TEXT NOT NULL,
|
|
59
|
+
browser_id TEXT NOT NULL,
|
|
60
|
+
driver_tab_id TEXT,
|
|
61
|
+
state TEXT NOT NULL
|
|
62
|
+
CHECK (state IN ('opening', 'open', 'closing', 'closed', 'failed')),
|
|
63
|
+
opened_at TEXT,
|
|
64
|
+
closed_at TEXT,
|
|
65
|
+
close_failed INTEGER NOT NULL DEFAULT 0
|
|
66
|
+
CHECK (close_failed IN (0, 1)),
|
|
67
|
+
close_attempts INTEGER NOT NULL DEFAULT 0
|
|
68
|
+
CHECK (close_attempts >= 0),
|
|
69
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
70
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
71
|
+
FOREIGN KEY (claim_id, browser_id) REFERENCES claims (id, browser_id),
|
|
72
|
+
-- Narrowed to the live states, which are the ones the partial unique index
|
|
73
|
+
-- ranges over. A tab still live must say whether it has a driver name; one
|
|
74
|
+
-- that has finished may have ended before it ever acquired one.
|
|
75
|
+
CHECK (
|
|
76
|
+
state NOT IN ('opening', 'open', 'closing')
|
|
77
|
+
OR (state = 'opening') = (driver_tab_id IS NULL)
|
|
78
|
+
)
|
|
79
|
+
) STRICT
|
|
80
|
+
`,
|
|
81
|
+
`INSERT INTO tabs_next SELECT * FROM tabs`,
|
|
82
|
+
`DROP TABLE tabs`,
|
|
83
|
+
`ALTER TABLE tabs_next RENAME TO tabs`,
|
|
84
|
+
// Rebuilt with the table, because dropping it took them with it. Identical
|
|
85
|
+
// to step one's, which is what keeps this a change to one constraint.
|
|
86
|
+
`CREATE UNIQUE INDEX one_row_per_physical_tab
|
|
87
|
+
ON tabs (browser_id, driver_tab_id) WHERE state IN ('opening', 'open', 'closing')`,
|
|
88
|
+
`CREATE INDEX tabs_claim ON tabs (claim_id)`,
|
|
89
|
+
];
|
|
90
|
+
/** Every statement this step runs, in order. */
|
|
91
|
+
export const STEP_FOUR_SQL = REBUILD;
|
|
92
|
+
export const stepFour = {
|
|
93
|
+
version: 4,
|
|
94
|
+
summary: 'A tab may end without ever having opened; the driver-name rule narrows to live states.',
|
|
95
|
+
apply: (db) => {
|
|
96
|
+
for (const statement of STEP_FOUR_SQL) {
|
|
97
|
+
db.exec(statement);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step five: `storage_seeded` becomes a recordable event kind (row #65).
|
|
3
|
+
*
|
|
4
|
+
* **A new step rather than an edit to step one**, per the rule that a step
|
|
5
|
+
* which has run somewhere is history (`CLAUDE.md`, `SCHEMA.md` §1.2d). SQLite
|
|
6
|
+
* cannot alter a check in place, so the table is rebuilt — which is why this
|
|
7
|
+
* step is longer than the one value it adds.
|
|
8
|
+
*
|
|
9
|
+
* ── Why the seed needs a kind of its own ────────────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* `SCHEMA.md` §3.2 asks a specific question of the ledger: *"which leases
|
|
12
|
+
* started life already holding a credential"*. That is a question about a
|
|
13
|
+
* category of lease, so it has to be answerable by selecting on one — and a
|
|
14
|
+
* seed folded into the `claim_granted` row's detail would only be findable by
|
|
15
|
+
* reading every grant's JSON and testing for a key. **A kind is what makes it
|
|
16
|
+
* a query rather than a scan**, which is the same argument §1.6 makes for the
|
|
17
|
+
* kind column being a fixed list at all.
|
|
18
|
+
*
|
|
19
|
+
* It is recorded as its own row rather than as a field on the grant for a
|
|
20
|
+
* second reason: the seed is applied **after** the arbitration transaction
|
|
21
|
+
* commits, because writing storage is browser work and browser work never
|
|
22
|
+
* happens inside the transaction (§2.4b). The grant row is written before the
|
|
23
|
+
* seeding has happened, so a field on it would be a claim about the future.
|
|
24
|
+
*
|
|
25
|
+
* ── What the row carries, and what it must never carry ──────────────────
|
|
26
|
+
*
|
|
27
|
+
* **Origins and keys, never values.** §3.2 states it outright and gives the
|
|
28
|
+
* reason §3.9 gives about cookie values: the question needs an answer and the
|
|
29
|
+
* answer does not need the credential in it. Nothing in the schema can
|
|
30
|
+
* enforce that — `detail` is text — so it is enforced where the row is built
|
|
31
|
+
* (`storage-seed.ts`) and asserted by a test that reads the ledger back and
|
|
32
|
+
* looks for the value it seeded.
|
|
33
|
+
*/
|
|
34
|
+
const REBUILD = [
|
|
35
|
+
`
|
|
36
|
+
CREATE TABLE events_next (
|
|
37
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
38
|
+
at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
39
|
+
kind TEXT NOT NULL
|
|
40
|
+
CHECK (kind IN (
|
|
41
|
+
'claim_requested', 'claim_granted', 'claim_queued', 'claim_promoted',
|
|
42
|
+
'claim_renewed', 'claim_released', 'claim_expired', 'claim_revoked',
|
|
43
|
+
'tab_opening', 'tab_open_failed', 'tab_closing',
|
|
44
|
+
'navigate', 'act', 'read', 'evaluate', 'capture', 'compare',
|
|
45
|
+
'browser_launched', 'browser_adopted', 'browser_exited',
|
|
46
|
+
'launch_race_lost', 'sweep',
|
|
47
|
+
-- Added by this step. The origins and the keys of a seeded
|
|
48
|
+
-- lease, and never the values.
|
|
49
|
+
'storage_seeded'
|
|
50
|
+
)),
|
|
51
|
+
outcome TEXT NOT NULL
|
|
52
|
+
CHECK (outcome IN ('allow', 'deny')),
|
|
53
|
+
guard TEXT,
|
|
54
|
+
claim_id TEXT REFERENCES claims (id),
|
|
55
|
+
tab_id TEXT REFERENCES tabs (id),
|
|
56
|
+
session_id TEXT,
|
|
57
|
+
adapter TEXT NOT NULL
|
|
58
|
+
CHECK (adapter IN ('tool-stdio', 'tool-http', 'cli', 'internal')),
|
|
59
|
+
browser_id TEXT REFERENCES browsers (id),
|
|
60
|
+
detail TEXT,
|
|
61
|
+
-- A guard names the rule that refused, so it belongs on a denial and means
|
|
62
|
+
-- nothing on an allow.
|
|
63
|
+
CHECK ((outcome = 'deny') = (guard IS NOT NULL))
|
|
64
|
+
) STRICT
|
|
65
|
+
`,
|
|
66
|
+
// The identifier is a cursor callers may already hold (§1.6), so the copy
|
|
67
|
+
// preserves it rather than letting AUTOINCREMENT reissue from one.
|
|
68
|
+
`INSERT INTO events_next SELECT * FROM events`,
|
|
69
|
+
`DROP TABLE events`,
|
|
70
|
+
`ALTER TABLE events_next RENAME TO events`,
|
|
71
|
+
// Rebuilt with the table, because dropping it took them with it. Identical
|
|
72
|
+
// to step one's, which is what keeps this a change to one check constraint
|
|
73
|
+
// and nothing else — a rebuild that quietly loses an index is a rebuild
|
|
74
|
+
// that changes the performance of every ledger read.
|
|
75
|
+
`CREATE INDEX events_at ON events (at)`,
|
|
76
|
+
`CREATE INDEX events_claim_id ON events (claim_id, id)`,
|
|
77
|
+
`CREATE INDEX events_kind_at ON events (kind, at)`,
|
|
78
|
+
`CREATE INDEX events_guard ON events (guard) WHERE guard IS NOT NULL`,
|
|
79
|
+
];
|
|
80
|
+
/** Every statement this step runs, in order. */
|
|
81
|
+
export const STEP_FIVE_SQL = REBUILD;
|
|
82
|
+
export const stepFive = {
|
|
83
|
+
version: 5,
|
|
84
|
+
summary: 'A seeded lease is recordable: the `storage_seeded` event kind (#65).',
|
|
85
|
+
apply: (db) => {
|
|
86
|
+
for (const statement of STEP_FIVE_SQL) {
|
|
87
|
+
db.exec(statement);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step six: signing in becomes recordable — `browser_signin_began` and
|
|
3
|
+
* `browser_signin_ended` (`SCHEMA.md` §5.5.1).
|
|
4
|
+
*
|
|
5
|
+
* **A new step rather than an edit to step five**, per the rule that a step
|
|
6
|
+
* which has run somewhere is history (`CLAUDE.md`, `SCHEMA.md` §1.2d). SQLite
|
|
7
|
+
* cannot alter a check in place, so the table is rebuilt — which is why this
|
|
8
|
+
* step is longer than the two values it adds.
|
|
9
|
+
*
|
|
10
|
+
* ── Why signing in needs kinds of its own ───────────────────────────────
|
|
11
|
+
*
|
|
12
|
+
* §5.5.1 makes signing in a **service operation** rather than something a
|
|
13
|
+
* person does to a browser directly, and the reason it gives is the one that
|
|
14
|
+
* makes these rows necessary: it *"refuses if any live lease holds a tab on
|
|
15
|
+
* that browser"*, and while it is in progress *"requests for it are refused
|
|
16
|
+
* with a retry hint"*. So the service spends a period deliberately turning
|
|
17
|
+
* callers away, and §1.6 requires **every decision, allowed and refused
|
|
18
|
+
* alike**.
|
|
19
|
+
*
|
|
20
|
+
* Without a kind for the two edges, that period is invisible in the ledger:
|
|
21
|
+
* a reader would see a run of `browser_unavailable` denials with nothing
|
|
22
|
+
* saying why the browser was unavailable, and the obvious reading of a
|
|
23
|
+
* cluster of denials is a fault. **These two rows are what make the
|
|
24
|
+
* difference between "the browser was broken" and "a person was signing in"
|
|
25
|
+
* a query rather than a guess** — which is the same argument §1.6 makes for
|
|
26
|
+
* the kind column being a fixed list at all.
|
|
27
|
+
*
|
|
28
|
+
* Two kinds rather than one with a direction field, because the pair brackets
|
|
29
|
+
* an interval and the questions asked of it are asked of the edges: when did
|
|
30
|
+
* this start, did it ever finish, is one open right now. A single kind would
|
|
31
|
+
* make "is a sign-in open" a scan that reads every row's JSON and counts,
|
|
32
|
+
* rather than a comparison of two counts.
|
|
33
|
+
*
|
|
34
|
+
* ── What the rows carry, and what they must never carry ─────────────────
|
|
35
|
+
*
|
|
36
|
+
* **Which browser, and when. Never anything a person typed.** A sign-in is
|
|
37
|
+
* the one moment where credentials are physically present at the machine, and
|
|
38
|
+
* the ledger is a file that gets read, copied and pasted into messages. What
|
|
39
|
+
* a sign-in produces lives in the browser's own profile directory (§1.2) and
|
|
40
|
+
* is written there by the browser; **nothing in this service ever sees it**,
|
|
41
|
+
* so there is nothing here to leak by accident — but the rule is stated
|
|
42
|
+
* because the row is built by hand and a well-meaning addition would be the
|
|
43
|
+
* way it stopped being true. Nothing in the schema can enforce it — `detail`
|
|
44
|
+
* is text — so it is enforced where the row is built (`sign-in.ts`) and
|
|
45
|
+
* asserted by a test that reads the ledger back.
|
|
46
|
+
*/
|
|
47
|
+
const REBUILD = [
|
|
48
|
+
`
|
|
49
|
+
CREATE TABLE events_next (
|
|
50
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
51
|
+
at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
52
|
+
kind TEXT NOT NULL
|
|
53
|
+
CHECK (kind IN (
|
|
54
|
+
'claim_requested', 'claim_granted', 'claim_queued', 'claim_promoted',
|
|
55
|
+
'claim_renewed', 'claim_released', 'claim_expired', 'claim_revoked',
|
|
56
|
+
'tab_opening', 'tab_open_failed', 'tab_closing',
|
|
57
|
+
'navigate', 'act', 'read', 'evaluate', 'capture', 'compare',
|
|
58
|
+
'browser_launched', 'browser_adopted', 'browser_exited',
|
|
59
|
+
'launch_race_lost', 'sweep', 'storage_seeded',
|
|
60
|
+
-- Added by this step. The two edges of the one interval in
|
|
61
|
+
-- which this service turns callers away on purpose, so that
|
|
62
|
+
-- period is legible as a sign-in rather than as a fault.
|
|
63
|
+
'browser_signin_began', 'browser_signin_ended'
|
|
64
|
+
)),
|
|
65
|
+
outcome TEXT NOT NULL
|
|
66
|
+
CHECK (outcome IN ('allow', 'deny')),
|
|
67
|
+
guard TEXT,
|
|
68
|
+
claim_id TEXT REFERENCES claims (id),
|
|
69
|
+
tab_id TEXT REFERENCES tabs (id),
|
|
70
|
+
session_id TEXT,
|
|
71
|
+
adapter TEXT NOT NULL
|
|
72
|
+
CHECK (adapter IN ('tool-stdio', 'tool-http', 'cli', 'internal')),
|
|
73
|
+
browser_id TEXT REFERENCES browsers (id),
|
|
74
|
+
detail TEXT,
|
|
75
|
+
-- A guard names the rule that refused, so it belongs on a denial and means
|
|
76
|
+
-- nothing on an allow.
|
|
77
|
+
CHECK ((outcome = 'deny') = (guard IS NOT NULL))
|
|
78
|
+
) STRICT
|
|
79
|
+
`,
|
|
80
|
+
// The identifier is a cursor callers may already hold (§1.6), so the copy
|
|
81
|
+
// preserves it rather than letting AUTOINCREMENT reissue from one.
|
|
82
|
+
`INSERT INTO events_next SELECT * FROM events`,
|
|
83
|
+
`DROP TABLE events`,
|
|
84
|
+
`ALTER TABLE events_next RENAME TO events`,
|
|
85
|
+
// Rebuilt with the table, because dropping it took them with it. Identical
|
|
86
|
+
// to step one's, which is what keeps this a change to one check constraint
|
|
87
|
+
// and nothing else — a rebuild that quietly loses an index is a rebuild
|
|
88
|
+
// that changes the performance of every ledger read.
|
|
89
|
+
`CREATE INDEX events_at ON events (at)`,
|
|
90
|
+
`CREATE INDEX events_claim_id ON events (claim_id, id)`,
|
|
91
|
+
`CREATE INDEX events_kind_at ON events (kind, at)`,
|
|
92
|
+
`CREATE INDEX events_guard ON events (guard) WHERE guard IS NOT NULL`,
|
|
93
|
+
];
|
|
94
|
+
/** Every statement this step runs, in order. */
|
|
95
|
+
export const STEP_SIX_SQL = REBUILD;
|
|
96
|
+
export const stepSix = {
|
|
97
|
+
version: 6,
|
|
98
|
+
summary: 'Signing in is recordable: the two `browser_signin_*` event kinds (§5.5.1).',
|
|
99
|
+
apply: (db) => {
|
|
100
|
+
for (const statement of STEP_SIX_SQL) {
|
|
101
|
+
db.exec(statement);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step seven: a browser can be claimed for a person before it has a process.
|
|
3
|
+
*
|
|
4
|
+
* ── The constraint this widens, and why it was right to begin with ──────
|
|
5
|
+
*
|
|
6
|
+
* Step one states it as `(state = 'stopped') = (pid IS NULL)`, with the
|
|
7
|
+
* reason that *"a stopped browser has no process, and a running one has
|
|
8
|
+
* one"*, expressed as a constraint rather than a convention because the
|
|
9
|
+
* reclamation path branches on the process being absent. Every state that
|
|
10
|
+
* existed when it was written was either *stopped*, or *a process is doing
|
|
11
|
+
* something*, so the biconditional was exactly true.
|
|
12
|
+
*
|
|
13
|
+
* ── What `signing-in` turned out to be, which the constraint did not cover ──
|
|
14
|
+
*
|
|
15
|
+
* `SCHEMA.md` §5.5.1 makes signing in a claim over the **profile**, not over
|
|
16
|
+
* a process: *"What the command does is claim the browser for the person"*,
|
|
17
|
+
* and *"nothing is stopped and nothing is relaunched"*. The claim has to be
|
|
18
|
+
* taken **before** a window is handed over, because its whole purpose is to
|
|
19
|
+
* stop a caller opening a tab in a browser somebody is about to drive by
|
|
20
|
+
* hand — a claim taken after the window appeared would be a guard that
|
|
21
|
+
* announced itself once the race was already lost.
|
|
22
|
+
*
|
|
23
|
+
* So there is a real and ordinary state that constraint makes unwritable:
|
|
24
|
+
* **the browser is not running, and a person has claimed it in order to sign
|
|
25
|
+
* in.** That is what a first sign-in on a fresh installation *is* — nobody
|
|
26
|
+
* has started a browser, which is precisely why nobody is signed in. The
|
|
27
|
+
* store rejected it with a check-constraint failure, which is the correct
|
|
28
|
+
* behaviour of a constraint that had been told something narrower than the
|
|
29
|
+
* truth.
|
|
30
|
+
*
|
|
31
|
+
* ── What is kept, and it is the part that was load-bearing ──────────────
|
|
32
|
+
*
|
|
33
|
+
* The direction the reclamation path relies on is **unchanged and still
|
|
34
|
+
* total**: a row that says `stopped` still has no process, and a row with a
|
|
35
|
+
* process still does not say `stopped`. Nothing that branches on *is there a
|
|
36
|
+
* process to reclaim* sees a different answer than before.
|
|
37
|
+
*
|
|
38
|
+
* What is relaxed is only the other direction, and only for one state: a
|
|
39
|
+
* browser that is `signing-in` may have a process or may not, because a
|
|
40
|
+
* person can be signing into one that is already up or into one that is
|
|
41
|
+
* being started for them. Every other non-stopped state still requires a
|
|
42
|
+
* process, so this does not become "the constraint means nothing now" —
|
|
43
|
+
* `starting`, `running` and `failed` are each still tied to one.
|
|
44
|
+
*
|
|
45
|
+
* **A new step rather than an edit to step one**, per the rule that a step
|
|
46
|
+
* which has run somewhere is history. SQLite cannot alter a check in place,
|
|
47
|
+
* so the table is rebuilt.
|
|
48
|
+
*/
|
|
49
|
+
const REBUILD = [
|
|
50
|
+
`
|
|
51
|
+
CREATE TABLE browsers_next (
|
|
52
|
+
id TEXT PRIMARY KEY
|
|
53
|
+
CHECK (id IN ('regular', 'private')),
|
|
54
|
+
state TEXT NOT NULL DEFAULT 'stopped'
|
|
55
|
+
CHECK (state IN ('stopped', 'starting', 'running', 'signing-in', 'failed')),
|
|
56
|
+
pid INTEGER,
|
|
57
|
+
launched_at TEXT,
|
|
58
|
+
endpoint TEXT,
|
|
59
|
+
browser_uuid TEXT,
|
|
60
|
+
restart_count INTEGER NOT NULL DEFAULT 0
|
|
61
|
+
CHECK (restart_count >= 0),
|
|
62
|
+
-- **The defaults are part of the column**, and a rebuild that omits them
|
|
63
|
+
-- silently changes what an INSERT that does not name them does: it stops
|
|
64
|
+
-- defaulting from the database's own clock and starts failing NOT NULL.
|
|
65
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
66
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
67
|
+
-- **A stopped browser has no process.** Unchanged from step one, total,
|
|
68
|
+
-- and it is the direction the reclamation path branches on.
|
|
69
|
+
CHECK (state != 'stopped' OR pid IS NULL),
|
|
70
|
+
-- **A browser with no process is stopped — unless a person has claimed it.**
|
|
71
|
+
-- Step one's other direction, with the single exception §5.5.1 requires:
|
|
72
|
+
-- signing in is a claim over the profile rather than over a process, taken
|
|
73
|
+
-- before a window is handed over, so it is the one state that may hold
|
|
74
|
+
-- either. Starting, running and failed each still require a process.
|
|
75
|
+
CHECK (pid IS NOT NULL OR state IN ('stopped', 'signing-in'))
|
|
76
|
+
) STRICT
|
|
77
|
+
`,
|
|
78
|
+
`INSERT INTO browsers_next SELECT * FROM browsers`,
|
|
79
|
+
`DROP TABLE browsers`,
|
|
80
|
+
`ALTER TABLE browsers_next RENAME TO browsers`,
|
|
81
|
+
];
|
|
82
|
+
/** Every statement this step runs, in order. */
|
|
83
|
+
export const STEP_SEVEN_SQL = REBUILD;
|
|
84
|
+
export const stepSeven = {
|
|
85
|
+
version: 7,
|
|
86
|
+
summary: 'A browser may be claimed for a person before it has a process (§5.5.1).',
|
|
87
|
+
apply: (db) => {
|
|
88
|
+
for (const statement of STEP_SEVEN_SQL) {
|
|
89
|
+
db.exec(statement);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step eight: a sign-in records **which process is holding it**.
|
|
3
|
+
*
|
|
4
|
+
* ── The failure this exists to make recoverable ─────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* `broker login` moves the browser to `signing-in` and gives it back in a
|
|
7
|
+
* `finally`. **A `finally` does not run when the process is signalled**, so a
|
|
8
|
+
* person who pressed Ctrl-C — the ordinary way anybody stops a command that is
|
|
9
|
+
* waiting — left the browser in `signing-in` with nothing able to move it out.
|
|
10
|
+
* Every caller was then refused `browser.serving` forever, and a second
|
|
11
|
+
* `broker login` was refused too, because §5.5.1 permits only one sign-in at a
|
|
12
|
+
* time. The state was reachable by one keystroke and escapable only by editing
|
|
13
|
+
* the database by hand.
|
|
14
|
+
*
|
|
15
|
+
* A signal handler is added alongside this step and covers the interruption
|
|
16
|
+
* case. **It cannot cover the rest**, and that is the whole reason this column
|
|
17
|
+
* exists: `SIGKILL` cannot be handled at all, a power cut runs nothing, and a
|
|
18
|
+
* process that dies in the kernel runs nothing either. Any mechanism that
|
|
19
|
+
* relies on the dying process doing something is a mechanism with a hole in
|
|
20
|
+
* exactly the cases nobody can rehearse.
|
|
21
|
+
*
|
|
22
|
+
* ── Why a new column rather than reusing `browsers.pid` ─────────────────
|
|
23
|
+
*
|
|
24
|
+
* **`browsers.pid` is the browser's process, not the sign-in's**, and the two
|
|
25
|
+
* are different processes with different lifetimes: the browser is adopted and
|
|
26
|
+
* deliberately outlives the command (`login-command.ts` detaches precisely so
|
|
27
|
+
* the person's window survives). Reading it as the owner would ask "is the
|
|
28
|
+
* browser still up" while meaning "is the person's command still running", and
|
|
29
|
+
* those answers diverge in both directions.
|
|
30
|
+
*
|
|
31
|
+
* Worse, step seven made `signing-in` the one state permitted a **null** pid,
|
|
32
|
+
* because a first sign-in on a fresh installation claims a browser that has
|
|
33
|
+
* never been started. That is the commonest sign-in there is, and it is
|
|
34
|
+
* precisely the one where `browsers.pid` carries nothing to test.
|
|
35
|
+
*
|
|
36
|
+
* ── Why not the lease sweep ─────────────────────────────────────────────
|
|
37
|
+
*
|
|
38
|
+
* The sweep expires **claims** whose `expires_at` has passed. A sign-in is not
|
|
39
|
+
* a claim: it takes no lease and no tab budget (§5.5.1), it is a state on the
|
|
40
|
+
* `browsers` row, and it has no expiry — deliberately, because a person
|
|
41
|
+
* signing in may take as long as they take and a sweep that timed them out
|
|
42
|
+
* would end a sign-in that was going fine. So there is no lapse for the sweep
|
|
43
|
+
* to notice, and extending it to notice one would mean inventing a deadline
|
|
44
|
+
* the design does not have.
|
|
45
|
+
*
|
|
46
|
+
* The question is settled by **evidence** instead: the owner is either running
|
|
47
|
+
* or it is not, which is a fact rather than a timer.
|
|
48
|
+
*
|
|
49
|
+
* ── Nullable, and what each value means ─────────────────────────────────
|
|
50
|
+
*
|
|
51
|
+
* - **Null while the browser is not signing in.** There is no owner, so
|
|
52
|
+
* recording one would be a claim about a process that is not doing anything.
|
|
53
|
+
* - **Set for the duration of a sign-in**, to the process identifier of the
|
|
54
|
+
* command that began it.
|
|
55
|
+
*
|
|
56
|
+
* It is deliberately **not** constrained to be non-null on `signing-in`. A
|
|
57
|
+
* store written by an older build carries `signing-in` rows with no owner, and
|
|
58
|
+
* a constraint would make this step fail on exactly the installations that hit
|
|
59
|
+
* the bug. Those rows read as "owner unknown", which the recovery path reports
|
|
60
|
+
* as such rather than reclaiming on a guess.
|
|
61
|
+
*
|
|
62
|
+
* **A new step rather than an edit to step seven**, per the rule in
|
|
63
|
+
* `steps.ts`: a step that has run somewhere is history.
|
|
64
|
+
*/
|
|
65
|
+
const ADD_COLUMN = `ALTER TABLE browsers ADD COLUMN signin_owner_pid INTEGER`;
|
|
66
|
+
/** Every statement this step runs, in order. */
|
|
67
|
+
export const STEP_EIGHT_SQL = [ADD_COLUMN];
|
|
68
|
+
export const stepEight = {
|
|
69
|
+
version: 8,
|
|
70
|
+
summary: 'A sign-in records the process holding it, so an abandoned one is recoverable (§5.5.1).',
|
|
71
|
+
apply: (db) => {
|
|
72
|
+
for (const statement of STEP_EIGHT_SQL) {
|
|
73
|
+
db.exec(statement);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step nine: a browser's name is free, and its kind is the column.
|
|
3
|
+
*
|
|
4
|
+
* ── What this drops, and it is a decision rather than a widening ────────
|
|
5
|
+
*
|
|
6
|
+
* Step one wrote `CHECK (id IN ('regular', 'private'))` and step seven
|
|
7
|
+
* repeated it through a rebuild. That check is the database's half of
|
|
8
|
+
* *"exactly two browsers, no exceptions, ever"* — the half that makes the
|
|
9
|
+
* sentence true from underneath, since the primary key caps the table at two
|
|
10
|
+
* and step one's seed floors it at two.
|
|
11
|
+
*
|
|
12
|
+
* **That sentence is overturned** (`DECISIONS.md` §13i): the configured
|
|
13
|
+
* browsers are a bounded list per kind rather than a fixed pair, so the set
|
|
14
|
+
* of legal names is not knowable when the schema is written. A check
|
|
15
|
+
* naming two literals cannot express *"a name somebody configured"*, and a
|
|
16
|
+
* check that tried would have to be rewritten every time a name changed —
|
|
17
|
+
* which is a schema step per configuration edit, on a table whose whole
|
|
18
|
+
* purpose is to be configured.
|
|
19
|
+
*
|
|
20
|
+
* ── What stands in its place, so nothing load-bearing is merely dropped ────
|
|
21
|
+
*
|
|
22
|
+
* **`kind` is a column with its own check.** What that constraint
|
|
23
|
+
* actually protects is not the two words: it is that every row is either a
|
|
24
|
+
* persistent signed-in browser or an ephemeral clean-room one, with no third
|
|
25
|
+
* thing and no row that is neither. That property is what the rest of
|
|
26
|
+
* the design branches on — a clean-room browser is launched with an
|
|
27
|
+
* ephemeral profile, a signed-in one is not, and `SCHEMA.md` §5.5.1's
|
|
28
|
+
* sign-in is a claim over a persistent profile.
|
|
29
|
+
*
|
|
30
|
+
* So the kind stays **database-enforced and total** while the name becomes
|
|
31
|
+
* free. This is the same trade §1.2 already made in the other direction:
|
|
32
|
+
* *"No `persistent` flag. Whether a browser uses a persistent profile is a
|
|
33
|
+
* property of which browser it is."* That reasoning held while the name was
|
|
34
|
+
* one of two literals and the word carried the kind. Once a name is
|
|
35
|
+
* `checkout` or `admin`, the word carries nothing, and the property has to
|
|
36
|
+
* be written down or it is not enforced anywhere. **The column exists
|
|
37
|
+
* because the name stopped implying it**, which is the condition §1.2's
|
|
38
|
+
* reasoning was conditional on all along.
|
|
39
|
+
*
|
|
40
|
+
* ── Backfill: the two existing rows are named by what they are ──────────
|
|
41
|
+
*
|
|
42
|
+
* A store stepped by an earlier build holds exactly the rows `regular` and
|
|
43
|
+
* `private`, because that is what the check permitted. Each is backfilled to
|
|
44
|
+
* its own kind by name, which is exact rather than a guess: those two names
|
|
45
|
+
* meant those two kinds, and no other name could exist to be ambiguous.
|
|
46
|
+
*
|
|
47
|
+
* The default is deliberately **absent**. A default would let a later insert
|
|
48
|
+
* that forgot the column produce a row whose kind is a guess, and the row
|
|
49
|
+
* creation this step enables (`DECISIONS.md` §13i — rows are created on
|
|
50
|
+
* first launch, not from configuration at startup) is exactly the writer
|
|
51
|
+
* that must state it.
|
|
52
|
+
*
|
|
53
|
+
* ── Rows are still not created from configuration here ──────────────────
|
|
54
|
+
*
|
|
55
|
+
* This step creates no rows and deletes none. It does not read the
|
|
56
|
+
* configured lists, and could not usefully: two processes on one machine may
|
|
57
|
+
* hold different configurations, and a step that seeded from one process's
|
|
58
|
+
* environment would write that process's beliefs into a store the other one
|
|
59
|
+
* shares. §1.2a already arbitrates the launch race through the same
|
|
60
|
+
* transaction as claims — *"one row, one winner"* — so a row appears when a
|
|
61
|
+
* browser is first launched, created by whichever caller won that race.
|
|
62
|
+
*
|
|
63
|
+
* The two rows step one seeded stay exactly where they are. They are the
|
|
64
|
+
* default configuration's two browsers, and a store that has them is a store
|
|
65
|
+
* that was stepped.
|
|
66
|
+
*
|
|
67
|
+
* **A new step rather than an edit to step one or step seven**, per the rule
|
|
68
|
+
* in `steps.ts`: a step that has run somewhere is history. SQLite cannot
|
|
69
|
+
* alter a check in place, so the table is rebuilt — the same shape step
|
|
70
|
+
* seven used, and for the same reason.
|
|
71
|
+
*/
|
|
72
|
+
const REBUILD = [
|
|
73
|
+
`
|
|
74
|
+
CREATE TABLE browsers_next (
|
|
75
|
+
-- **No check on the name.** The legal names are the configured ones, which
|
|
76
|
+
-- the schema cannot know. What a name must satisfy is enforced where it is
|
|
77
|
+
-- read: 'src/config/environment.ts' refuses a name that is not a usable
|
|
78
|
+
-- word, once, at startup, naming the entry that was wrong.
|
|
79
|
+
id TEXT PRIMARY KEY,
|
|
80
|
+
-- **The kind, and it is total.** Every row is a persistent signed-in
|
|
81
|
+
-- browser or an ephemeral clean-room one; there is no third and no row
|
|
82
|
+
-- that is neither. This is what the name used to carry.
|
|
83
|
+
kind TEXT NOT NULL
|
|
84
|
+
CHECK (kind IN ('regular', 'private')),
|
|
85
|
+
state TEXT NOT NULL DEFAULT 'stopped'
|
|
86
|
+
CHECK (state IN ('stopped', 'starting', 'running', 'signing-in', 'failed')),
|
|
87
|
+
pid INTEGER,
|
|
88
|
+
launched_at TEXT,
|
|
89
|
+
endpoint TEXT,
|
|
90
|
+
browser_uuid TEXT,
|
|
91
|
+
restart_count INTEGER NOT NULL DEFAULT 0
|
|
92
|
+
CHECK (restart_count >= 0),
|
|
93
|
+
-- **The defaults are part of the column**, and a rebuild that omits them
|
|
94
|
+
-- silently changes what an INSERT that does not name them does: it stops
|
|
95
|
+
-- defaulting from the database's own clock and starts failing NOT NULL.
|
|
96
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
97
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
98
|
+
signin_owner_pid INTEGER,
|
|
99
|
+
-- **A stopped browser has no process.** Unchanged from step one, total,
|
|
100
|
+
-- and it is the direction the reclamation path branches on.
|
|
101
|
+
CHECK (state != 'stopped' OR pid IS NULL),
|
|
102
|
+
-- **A browser with no process is stopped — unless a person has claimed it.**
|
|
103
|
+
-- Step seven's exception, carried forward unchanged: signing in is a claim
|
|
104
|
+
-- over the profile rather than over a process, so it is the one state that
|
|
105
|
+
-- may hold either.
|
|
106
|
+
CHECK (pid IS NOT NULL OR state IN ('stopped', 'signing-in'))
|
|
107
|
+
) STRICT
|
|
108
|
+
`,
|
|
109
|
+
// The columns are named on both sides rather than `SELECT *`, because the
|
|
110
|
+
// destination carries a column the source does not: a positional copy
|
|
111
|
+
// would shift every value one place to the left from `kind` onward.
|
|
112
|
+
`
|
|
113
|
+
INSERT INTO browsers_next
|
|
114
|
+
(id, kind, state, pid, launched_at, endpoint, browser_uuid, restart_count,
|
|
115
|
+
created_at, updated_at, signin_owner_pid)
|
|
116
|
+
SELECT
|
|
117
|
+
id,
|
|
118
|
+
-- Exact rather than a guess: the check this step drops permitted these two
|
|
119
|
+
-- names and no others, and each named its own kind.
|
|
120
|
+
CASE id WHEN 'private' THEN 'private' ELSE 'regular' END,
|
|
121
|
+
state, pid, launched_at, endpoint, browser_uuid, restart_count,
|
|
122
|
+
created_at, updated_at, signin_owner_pid
|
|
123
|
+
FROM browsers
|
|
124
|
+
`,
|
|
125
|
+
`DROP TABLE browsers`,
|
|
126
|
+
`ALTER TABLE browsers_next RENAME TO browsers`,
|
|
127
|
+
];
|
|
128
|
+
/** Every statement this step runs, in order. */
|
|
129
|
+
export const STEP_NINE_SQL = REBUILD;
|
|
130
|
+
export const stepNine = {
|
|
131
|
+
version: 9,
|
|
132
|
+
summary: "A browser's name is configured and its kind is a column (§1.2, DECISIONS.md §13i).",
|
|
133
|
+
apply: (db) => {
|
|
134
|
+
for (const statement of STEP_NINE_SQL) {
|
|
135
|
+
db.exec(statement);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
};
|