char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
6183bc3
main
1import type { Environment } from "@berry/api" ; 2import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context" ; 3import { run } from "./runner.ts" ; 4 5type Reply = { 6id ?:number ; 7type ?:string ; 8result ?:unknown ; 9message ?:string ; 10error ?:string ; 11method ?:string ; 12params ?:Record < string , unknown >; 13}; 14 15export type Session = { 16request < T > ( method :string , params :object ) :Promise < T >; 17close () :Promise < void >; 18readonly closed :Promise < void >; 19}; 20 21/** A target-side Firefox session, spoken to through its runner's stdin and stdout. */ 22export async function launch ( 23env :Environment , 24onEvent :( method :string , params :Record < string , unknown >) => void , 25signal ?:AbortSignal , 26) :Promise < Session > { 27signal ?. throwIfAborted (); 28env . lifetime ?. throwIfAborted (); 29if ( env . platform === "win32" ) { 30throw new Error ( "The browser requires Firefox and a POSIX shell on the target machine." ); 31} 32const child = await env . spawn ( 33[ "deno" , "eval" , "--quiet" , "--no-config" , "--no-lock" , `await ( ${ run . toString ()} )();` ], 34{}, 35BACKGROUND_CONTEXT , 36); 37const input = child . stdin . getWriter (); 38const encoder = new TextEncoder (); 39const ready = Promise . withResolvers < void >(); 40const closed = Promise . withResolvers < void >(); 41const pending = new Map < number , { resolve ( value :unknown ) :void ; reject ( error :Error ) :void }>(); 42let nextId = 0 ; 43let failure :Error | undefined ; 44let stderr = "" ; 45let closing :Promise < void > | undefined ; 46const fail = ( error :unknown ) => { 47if ( failure ) return ; 48failure = error instanceof Error ?error :new Error ( String ( error )); 49ready . reject ( failure ); 50for ( const request of pending . values ()) request . reject ( failure ); 51pending . clear (); 52closed . resolve (); 53void close (); 54}; 55const close = () => 56closing ??= ( async () => { 57env . lifetime ?. removeEventListener ( "abort" , disconnected ); 58const timeout = setTimeout (() => child . kill ( "SIGKILL" ), 3_000 ); 59try { 60await input . close (). catch (() => {}); 61await child . exited ; 62await Promise . allSettled ([ reading , errors ]); 63} finally { 64clearTimeout ( timeout ); 65} 66})(); 67const disconnected = () => fail ( env . lifetime ?. reason ?? new Error ( "Target connection closed." )); 68const aborted = () => fail ( signal ! . reason ); 69const errors = ( async () => { 70const decoder = new TextDecoder (); 71for await ( const chunk of child . stderr ) { 72stderr = ( stderr + decoder . decode ( chunk , { stream :true })). slice ( - 16_384 ); 73} 74})(); 75void errors . catch ( fail ); 76const reading = ( async () => { 77const decoder = new TextDecoder (); 78let buffered = "" ; 79for await ( const chunk of child . stdout ) { 80buffered += decoder . decode ( chunk , { stream :true }); 81let newline :number ; 82while (( newline = buffered . indexOf ( "\n" )) >= 0 ) { 83const message = JSON . parse ( buffered . slice ( 0 , newline )) as Reply ; 84buffered = buffered . slice ( newline + 1 ); 85if ( message . type === "ready" ) ready . resolve (); 86else if ( message . id === undefined ) { 87if ( message . type === "error" ) throw new Error ( message . message ); 88if ( message . method && message . params ) onEvent ( message . method , message . params ); 89} else { 90const request = pending . get ( message . id ); 91if ( ! request ) continue ; 92pending . delete ( message . id ); 93if ( message . type === "success" ) request . resolve ( message . result ); 94else { 95request . reject ( 96new Error ( 97` ${ message . error ?? "Firefox error" } : ${ message . message ?? "Command failed" } ` , 98), 99); 100} 101} 102} 103} 104await errors . catch (() => {}); 105throw new Error ( stderr . trim () || "Firefox connection closed." ); 106})(); 107void reading . catch ( fail ); 108env . lifetime ?. addEventListener ( "abort" , disconnected , { once :true }); 109signal ?. addEventListener ( "abort" , aborted , { once :true }); 110const timeout = setTimeout ( 111() => fail ( new Error ( "Browser runner did not start within 25 seconds." )), 11225_000 , 113); 114try { 115if ( signal ?. aborted ) aborted (); 116if ( env . lifetime ?. aborted ) disconnected (); 117await ready . promise ; 118const session :Session = { 119closed :closed . promise , 120 close, 121request < T > ( method :string , params :object ) :Promise < T > { 122if ( failure || closing ) { 123return Promise . reject ( failure ?? new Error ( "Firefox connection closed." )); 124} 125const id = ++ nextId ; 126return new Promise < T >(( resolve , reject ) => { 127const timeout = setTimeout (() => { 128pending . delete ( id ); 129reject ( new Error ( `Firefox command ${ method } timed out.` )); 130}, 30_000 ); 131pending . set ( id , { 132resolve :( value ) => { 133clearTimeout ( timeout ); 134resolve ( value as T ); 135}, 136reject :( error ) => { 137clearTimeout ( timeout ); 138reject ( error ); 139}, 140}); 141void input . write ( encoder . encode ( JSON . stringify ({ id, method, params}) + "\n" )). catch ( 142fail , 143); 144}); 145}, 146}; 147await session . request ( "session.new" , { capabilities :{ alwaysMatch :{} } }); 148await session . request ( "session.subscribe" , { 149events :[ 150"log.entryAdded" , 151"network.fetchError" , 152"network.responseCompleted" , 153"browsingContext.navigationStarted" , 154"browsingContext.domContentLoaded" , 155"browsingContext.navigationFailed" , 156"browsingContext.fragmentNavigated" , 157], 158}); 159return session ; 160} catch ( error ) { 161await close (); 162throw error ; 163} finally { 164clearTimeout ( timeout ); 165signal ?. removeEventListener ( "abort" , aborted ); 166} 167}