char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Sombrowser: run Firefox in the target environment6183bc3

main
4.5 KiB133 linesraw
1/** A self-contained program sent to the target, owning Firefox and its native WebSocket. */
2export async function run(): Promise<never> {
3  // owns firefox and its profile, so both go once its stdin closes: when berry closes the session, or
4  // however berry exits. it ignores the terminal's signals and leaves shutting down to berry
5  const WATCHDOG = `
6profile=$(mktemp -d "\${TMPDIR:-/tmp}/berry-agent-browser-XXXXXXXX") || exit 1
7exec 3<&0
8firefox --profile "$profile" "$@" 3<&- </dev/null &
9browser=$!
10trap '' HUP INT TERM
11{ read -r _ <&3; kill $browser; sleep 2; kill -KILL $browser; rm -rf "$profile"; } 2>/dev/null &
12watcher=$!
13wait $browser
14kill -KILL $watcher 2>/dev/null
15rm -rf "$profile"
16`;
17  const child = new Deno.Command("sh", {
18    args: [
19      "-c",
20      WATCHDOG,
21      "berry-agent-browser",
22      "--headless",
23      "--no-remote",
24      "--remote-debugging-port",
25      "0",
26      "about:blank",
27    ],
28    stdin: "piped",
29    stdout: "null",
30    stderr: "piped",
31  }).spawn();
32  const output = Deno.stdout.writable.getWriter();
33  const encoder = new TextEncoder();
34  const stopped = Promise.withResolvers<void>();
35  let ended = false;
36  let failure: unknown;
37  let socket: WebSocket | undefined;
38  const stop = (error?: unknown) => {
39    if (ended) return;
40    ended = true;
41    failure = error;
42    stopped.resolve();
43  };
44  const send = (text: string) => output.write(encoder.encode(text + "\n"));
45  void (async () => {
46    let buffered = "";
47    for await (const chunk of Deno.stdin.readable.pipeThrough(new TextDecoderStream())) {
48      buffered += chunk;
49      let newline: number;
50      while ((newline = buffered.indexOf("\n")) >= 0) {
51        if (socket?.readyState !== WebSocket.OPEN) {
52          throw new Error("Firefox connection is not open.");
53        }
54        socket.send(buffered.slice(0, newline));
55        buffered = buffered.slice(newline + 1);
56      }
57    }
58    stop();
59  })().catch(stop);
60  try {
61    await Promise.race([
62      (async () => {
63        const endpoint = await new Promise<string>((resolve, reject) => {
64          const timeout = setTimeout(
65            () => reject(new Error("Firefox did not start within 10 seconds.")),
66            10_000,
67          );
68          let seen = "";
69          void (async () => {
70            try {
71              // drain even after finding the endpoint, or firefox can block on stderr
72              for await (const chunk of child.stderr.pipeThrough(new TextDecoderStream())) {
73                seen = (seen + chunk).slice(-16_384);
74                const match = seen.match(/WebDriver BiDi listening on (ws:\/\/[^\r\n]+)\r?\n/);
75                if (match) {
76                  clearTimeout(timeout);
77                  resolve(`${match[1]}/session`);
78                }
79              }
80              reject(new Error(seen.trim() || "Firefox exited before starting."));
81            } catch (error) {
82              reject(error);
83            } finally {
84              clearTimeout(timeout);
85            }
86          })();
87        });
88        if (ended) return;
89        const ws = socket = new WebSocket(endpoint);
90        ws.addEventListener("message", (event) => {
91          try {
92            void send(JSON.stringify(JSON.parse(String(event.data)))).catch(stop);
93          } catch (error) {
94            stop(error);
95          }
96        });
97        ws.addEventListener("close", () => stop(new Error("Firefox connection closed.")));
98        await new Promise<void>((resolve, reject) => {
99          const timeout = setTimeout(
100            () => reject(new Error("Firefox connection timed out.")),
101            10_000,
102          );
103          ws.addEventListener("open", () => {
104            clearTimeout(timeout);
105            resolve();
106          }, { once: true });
107          ws.addEventListener("error", () => {
108            clearTimeout(timeout);
109            reject(new Error("Could not connect to Firefox."));
110          }, { once: true });
111          ws.addEventListener("close", () => {
112            clearTimeout(timeout);
113            reject(new Error("Firefox connection closed."));
114          }, { once: true });
115        });
116        await send(JSON.stringify({ type: "ready" }));
117      })(),
118      stopped.promise,
119    ]);
120    await stopped.promise;
121  } catch (error) {
122    stop(error);
123  } finally {
124    if (failure) {
125      await send(JSON.stringify({ type: "error", message: String(failure) })).catch(() => {});
126    }
127    socket?.close();
128    await child.stdin.close().catch(() => {});
129    await child.status;
130    await output.close().catch(() => {});
131  }
132  Deno.exit(failure ? 1 : 0);
133}