@agent-flow-board/bridge 0.2.0 → 0.4.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 (54) hide show
  1. package/README.md +26 -0
  2. package/dist/checks/appdir.js +105 -0
  3. package/dist/checks/approve.js +274 -0
  4. package/dist/checks/askBoard.js +143 -0
  5. package/dist/checks/audit.js +3 -2
  6. package/dist/checks/bind.js +3 -2
  7. package/dist/checks/board.js +30 -0
  8. package/dist/checks/broker.js +439 -0
  9. package/dist/checks/browser.js +137 -32
  10. package/dist/checks/browserpool.js +221 -0
  11. package/dist/checks/cache.js +710 -0
  12. package/dist/checks/command.js +126 -87
  13. package/dist/checks/commands.js +315 -0
  14. package/dist/checks/connections.js +4 -3
  15. package/dist/checks/copy.js +320 -0
  16. package/dist/checks/dbpool.js +710 -0
  17. package/dist/checks/doctor.js +655 -0
  18. package/dist/checks/envPolicy.js +50 -1
  19. package/dist/checks/envrules.js +322 -0
  20. package/dist/checks/evidence.js +114 -0
  21. package/dist/checks/fsretry.js +29 -0
  22. package/dist/checks/importlegacy.js +300 -0
  23. package/dist/checks/install.js +150 -0
  24. package/dist/checks/live.js +76 -0
  25. package/dist/checks/lock.js +28 -4
  26. package/dist/checks/login.js +307 -0
  27. package/dist/checks/mailguard-hook.cjs +216 -0
  28. package/dist/checks/mailguard.js +98 -0
  29. package/dist/checks/paths.js +24 -0
  30. package/dist/checks/preview.js +418 -191
  31. package/dist/checks/profiles.js +110 -0
  32. package/dist/checks/provision.js +1 -1
  33. package/dist/checks/recipe.js +19 -5
  34. package/dist/checks/registry.js +548 -0
  35. package/dist/checks/repoenv.js +162 -0
  36. package/dist/checks/roots.js +144 -0
  37. package/dist/checks/routing.js +68 -0
  38. package/dist/checks/run.js +243 -53
  39. package/dist/checks/sandbox.js +32 -10
  40. package/dist/checks/scheduler.js +383 -0
  41. package/dist/checks/scope.js +119 -30
  42. package/dist/checks/sealedNames.js +39 -0
  43. package/dist/checks/secrets.js +454 -0
  44. package/dist/checks/secretscmd.js +80 -0
  45. package/dist/checks/service.js +589 -0
  46. package/dist/checks/signin.js +7 -1
  47. package/dist/checks/signinKey.js +104 -0
  48. package/dist/checks/singleflight.js +307 -0
  49. package/dist/checks/store.js +92 -0
  50. package/dist/checks/surface.js +124 -0
  51. package/dist/checks/warm.js +342 -0
  52. package/dist/checks/watch.js +190 -63
  53. package/dist/checks/wire.js +87 -0
  54. package/package.json +1 -1
@@ -0,0 +1,221 @@
1
+ // THE BROWSER POOL: RUNS THAT NEED THE SAME REACHABLE HOSTS SHARE ONE CHROMIUM SERVER, EACH RUN WITH A FRESH CONTEXT OF ITS OWN.
2
+ // Why sharing is safe: the server is started with Playwright's launchServer and never with the shared-browser option, so every connection to its endpoint
3
+ // gets contexts of its own and sees none of another connection's (proved by the opt-in real browser test). Why the key holds the hosts: the host
4
+ // resolver rule is a launch argument of the browser, so a browser started for one set of hosts can never reach the hosts of another set.
5
+ // A registration for a live site (`dedicated: true`) always gets a browser of its own.
6
+ //
7
+ // FILE LAYOUT (poolDir()): browser-<id>.json, one per launched browser, written whole when the browser is up and removed when it closes or crashes:
8
+ // { kind: 'browser', id, pid (the process that owns the pool), key, headed, hosts, dedicated, startedAt }
9
+ // The websocket endpoint is NOT written: it is a capability. Sharing one browser across processes is the machine broker's job; until it exists a pool
10
+ // lives in one process, and the marker only lets doctor see (and sweep, when the owner is dead) what was left.
11
+ // -- DEP-MODULES -- //
12
+ import { randomBytes } from 'node:crypto';
13
+ import { readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ // -- MODULES -- //
16
+ import { launchBrowserServer } from './browser.js';
17
+ import { pidAlive } from './lock.js';
18
+ import { ensureDir, poolDir } from './paths.js';
19
+ // -- CONSTANTS -- //
20
+ /** A shared browser with no runs on it stays up this long, so the next run of a burst does not pay for a new launch. */
21
+ const DEFAULT_IDLE_MS = 15_000;
22
+ const MARKER_PREFIX = 'browser-';
23
+ // -- FUNCTIONS -- //
24
+ /** The pool key: the sorted, lower-cased host set and the headed flag. Two runs share a browser only when this is equal. */
25
+ export function browserKey(hosts, headed) {
26
+ const set = [...new Set(hosts.map((h) => h.trim().toLowerCase()).filter(Boolean))].sort();
27
+ return `${headed ? 'headed' : 'headless'}|${set.join(',')}`;
28
+ }
29
+ function writeWhole(file, text) {
30
+ const tmp = `${file}.${process.pid}.${randomBytes(3).toString('hex')}.tmp`;
31
+ writeFileSync(tmp, text, { mode: 0o600 });
32
+ renameSync(tmp, file);
33
+ }
34
+ const markerFile = (id) => join(poolDir(), `${MARKER_PREFIX}${id}.json`);
35
+ /** Every browser marker on this machine, with whether the process that owns it is alive. For doctor. */
36
+ export function listBrowserMarkers() {
37
+ const out = [];
38
+ let names;
39
+ try {
40
+ names = readdirSync(poolDir());
41
+ }
42
+ catch {
43
+ return out;
44
+ }
45
+ for (const n of names) {
46
+ if (!n.startsWith(MARKER_PREFIX) || !n.endsWith('.json'))
47
+ continue;
48
+ try {
49
+ const v = JSON.parse(readFileSync(join(poolDir(), n), 'utf8'));
50
+ const m = v;
51
+ if (m && m.kind === 'browser' && typeof m.id === 'string' && typeof m.pid === 'number')
52
+ out.push({ marker: m, ownerAlive: pidAlive(m.pid) });
53
+ }
54
+ catch {
55
+ /* A damaged marker is not listed */
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+ /** Removes the markers of browsers whose owner process is gone (the browser died with it). Returns how many. */
61
+ export function sweepBrowserMarkers() {
62
+ let removed = 0;
63
+ for (const { marker, ownerAlive } of listBrowserMarkers()) {
64
+ if (ownerAlive || !/^[a-f0-9]{6,16}$/.test(marker.id))
65
+ continue;
66
+ rmSync(markerFile(marker.id), { force: true });
67
+ removed++;
68
+ }
69
+ return removed;
70
+ }
71
+ export class BrowserPool {
72
+ entries = new Map();
73
+ all = new Set();
74
+ launcher;
75
+ idleMs;
76
+ markers;
77
+ constructor(opts = {}) {
78
+ this.launcher = opts.launcher ?? ((o) => launchBrowserServer(o));
79
+ this.idleMs = opts.idleMs ?? DEFAULT_IDLE_MS;
80
+ this.markers = opts.markers ?? true;
81
+ }
82
+ /** How many browsers are up (or starting), counting dedicated ones. */
83
+ get size() {
84
+ return this.all.size;
85
+ }
86
+ launch(entry) {
87
+ entry.server = this.launcher({
88
+ headed: entry.request.headed,
89
+ hosts: entry.request.hosts
90
+ }).then((server) => {
91
+ if (this.markers)
92
+ this.writeMarker(entry);
93
+ // A crash takes only this entry out: the next acquire of its key launches a new browser, runs that hold this one fail on their own
94
+ server.onClose?.(() => this.markGone(entry));
95
+ return server;
96
+ }, (e) => {
97
+ this.markGone(entry);
98
+ throw e;
99
+ });
100
+ // The rejection is handled by the acquire that awaits it
101
+ entry.server.catch(() => { });
102
+ }
103
+ writeMarker(entry) {
104
+ try {
105
+ ensureDir(poolDir());
106
+ const marker = {
107
+ kind: 'browser',
108
+ id: entry.id,
109
+ pid: process.pid,
110
+ key: entry.key,
111
+ headed: entry.request.headed,
112
+ hosts: entry.request.hosts,
113
+ dedicated: entry.request.dedicated,
114
+ startedAt: new Date().toISOString()
115
+ };
116
+ writeWhole(markerFile(entry.id), JSON.stringify(marker) + '\n');
117
+ }
118
+ catch {
119
+ /* The marker is for doctor only: a browser still works without it */
120
+ }
121
+ }
122
+ markGone(entry) {
123
+ if (entry.gone)
124
+ return;
125
+ entry.gone = true;
126
+ if (entry.idleTimer)
127
+ clearTimeout(entry.idleTimer);
128
+ if (this.entries.get(entry.key) === entry)
129
+ this.entries.delete(entry.key);
130
+ this.all.delete(entry);
131
+ if (this.markers)
132
+ rmSync(markerFile(entry.id), { force: true });
133
+ }
134
+ async closeEntry(entry) {
135
+ this.markGone(entry);
136
+ try {
137
+ await (await entry.server).close();
138
+ }
139
+ catch {
140
+ /* Already closed or never started */
141
+ }
142
+ }
143
+ async acquire(req = {}) {
144
+ const headed = req.headed === true;
145
+ const hosts = [
146
+ ...new Set((req.hosts ?? []).map((h) => h.trim().toLowerCase()).filter(Boolean))
147
+ ].sort();
148
+ const dedicated = req.dedicated === true;
149
+ const key = browserKey(hosts, headed);
150
+ let entry = dedicated ? undefined : this.entries.get(key);
151
+ if (!entry || entry.gone) {
152
+ entry = {
153
+ id: randomBytes(4).toString('hex'),
154
+ key,
155
+ request: { headed, hosts, dedicated },
156
+ server: Promise.resolve(null),
157
+ refs: 0,
158
+ gone: false
159
+ };
160
+ if (!dedicated)
161
+ this.entries.set(key, entry);
162
+ this.all.add(entry);
163
+ this.launch(entry);
164
+ }
165
+ const mine = entry;
166
+ mine.refs++;
167
+ if (mine.idleTimer) {
168
+ clearTimeout(mine.idleTimer);
169
+ mine.idleTimer = undefined;
170
+ }
171
+ let server;
172
+ try {
173
+ server = await mine.server;
174
+ }
175
+ catch (e) {
176
+ mine.refs--;
177
+ throw e;
178
+ }
179
+ let released = false;
180
+ return {
181
+ wsEndpoint: () => server.wsEndpoint(),
182
+ release: async () => {
183
+ if (released)
184
+ return;
185
+ released = true;
186
+ mine.refs--;
187
+ if (mine.refs > 0 || mine.gone)
188
+ return;
189
+ if (dedicated || this.idleMs <= 0)
190
+ return this.closeEntry(mine);
191
+ // The grace period: a run that starts a moment later reuses this browser
192
+ mine.idleTimer = setTimeout(() => {
193
+ if (mine.refs === 0)
194
+ void this.closeEntry(mine);
195
+ }, this.idleMs);
196
+ mine.idleTimer.unref();
197
+ }
198
+ };
199
+ }
200
+ /** Closes every browser at once, whatever holds it. For the end of a worker or a run. */
201
+ async closeAll() {
202
+ await Promise.all([...this.all].map((e) => this.closeEntry(e)));
203
+ }
204
+ }
205
+ let shared = null;
206
+ /** The pool of this process. */
207
+ export function browserPool() {
208
+ shared ??= new BrowserPool();
209
+ return shared;
210
+ }
211
+ /**
212
+ * A browser for one run. The same sorted host set and headed flag share one Chromium; `dedicated` (a live site) never shares. Call `release()` when the
213
+ * run ends; the browser closes after a short idle grace once nobody holds it.
214
+ */
215
+ export function acquireBrowser(req = {}) {
216
+ return browserPool().acquire(req);
217
+ }
218
+ /** Closes every browser of this process's pool. */
219
+ export async function closeAllBrowsers() {
220
+ await shared?.closeAll();
221
+ }