char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
0853dde
main
1// Runs on the machine an environment names. berry starts it over ssh and bootstraps it on stdin; from then on, every 2// line in is a call on this machine's environment, and every line out what came of one. 3 4import { homedir } from "node:os" ; 5import process from "node:process" ; 6import { BACKGROUND_CONTEXT , withAbortSignal } from "@earendil-works/chord/context" ; 7import type { Context } from "@earendil-works/chord" ; 8import { FileError , type ShellExecOptions } from "@earendil-works/pi-durable/env" ; 9import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node" ; 10import { resolve } from "node:path" ; 11import { 12connectHere , 13type Process , 14type Pty , 15type PtyOptions , 16spawnHere , 17type SpawnOptions , 18} from "@berry/api/environment.ts" ; 19import { ptyHere } from "@berry/api/pty.ts" ; 20import { 21decode , 22encode , 23type FetchOptions , 24lines , 25PROTOCOL , 26type Request , 27type Response , 28} from "./codec.ts" ; 29 30/** What the bootstrap read of stdin before handing it on. */ 31type Input = { reader :ReadableStreamDefaultReader < Uint8Array >; buffered :Uint8Array }; 32const input = ( globalThis as { berryStdin ?:Input }). berryStdin ?? 33{ reader :Deno . stdin . readable . getReader (), buffered :new Uint8Array () }; 34 35const output = Deno . stdout . writable . getWriter (); 36const encoder = new TextEncoder (); 37let shuttingDown = false ; 38// berry gone: nothing to tell 39const send = ( message :Response ) => { 40if ( shuttingDown ) return ; 41void output . write ( encoder . encode ( ` ${ encode ( message )} \n` )). catch (() => {}); 42}; 43 44/** Methods that take paths and return a result, and nothing else. */ 45const PLAIN = new Set ([ 46"absolutePath" , 47"joinPath" , 48"readTextFile" , 49"readTextLines" , 50"readBinaryFile" , 51"writeFile" , 52"appendFile" , 53"truncateFile" , 54"flushFile" , 55"renameFile" , 56"fileInfo" , 57"listDir" , 58"canonicalPath" , 59"exists" , 60"createDir" , 61"remove" , 62"createTempDir" , 63"createTempFile" , 64]); 65 66/** Readers and watchers berry holds open, by number. */ 67const handles = new Map < number , Record < string , ( ...args :unknown []) => Promise < unknown >>>(); 68let handled = 0 ; 69const running = new Map < number , AbortController >(); 70const settling = new Set < Promise < unknown >>(); 71 72/** Programs and sockets berry talks to through their pipes, by number. */ 73const pipes = new Map < 74number , 75{ 76stdin :WritableStreamDefaultWriter < Uint8Array >; 77kill ( signal ?:"SIGTERM" | "SIGKILL" ) :void ; 78resize ?( cols :number , rows :number ) :void ; 79close () :Promise < void >; 80} 81>(); 82let piped = 0 ; 83 84async function settleWithin ( promise :Promise < unknown >, ms :number ) { 85let timer :ReturnType < typeof setTimeout > | undefined ; 86try { 87await Promise . race ([ 88promise . catch (() => {}), 89new Promise < void >(( resolve ) => timer = setTimeout ( resolve , ms )), 90]); 91} finally { 92clearTimeout ( timer ); 93} 94} 95 96/** Forward what `process` says to call `i`, until it exits. */ 97function pipe ( 98i :number , 99process :Process & Partial < Pick < Pty , "resize" >>, 100{ group= false , socket= false } = {}, 101) { 102const id = ++ piped ; 103const stdin = process . stdin . getWriter (); 104const readers = [ process . stdout . getReader (), process . stderr . getReader ()]; 105const forward = async ( reader :ReadableStreamDefaultReader < Uint8Array >, name :string ) => { 106try { 107while ( true ) { 108const { done, value} = await reader . read (); 109if ( done ) break ; 110send ({ i, o :[ name , value ] }); 111} 112} finally { 113send ({ i, o :[ name , null ] }); 114reader . releaseLock (); 115} 116}; 117const streaming = Promise . allSettled ([ 118forward ( readers [ 0 ], "stdout" ), 119forward ( readers [ 1 ], "stderr" ), 120]); 121let closing :Promise < void > | undefined ; 122const close = () => 123closing ??= ( async () => { 124process . kill ( "SIGKILL" ); 125await settleWithin ( 126Promise . allSettled ([ 127stdin . abort (), 128 ...readers . map (( reader ) => reader . cancel (). catch (() => {})), 129streaming , 130]), 1311000 , 132); 133})(); 134pipes . set ( id , { stdin, kill :process . kill , resize :process . resize , close}); 135const complete = ( async () => { 136if ( socket ) await streaming ; 137const code = await process . exited ; 138send ({ i, o :[ "exit" , code ] }); 139if ( group ) { 140process . kill (); 141await settleWithin ( streaming , 1000 ); 142// descendants cannot keep an exited leader's pipes or process group alive indefinitely 143process . kill ( "SIGKILL" ); 144await close (); 145} else { 146await streaming ; 147void stdin . abort (). catch (() => {}); 148} 149pipes . delete ( id ); 150send ({ i, o :[ "end" , null ] }); 151})(); 152void complete . catch (() => { 153send ({ i, o :[ "exit" , null ] }); 154void close (). finally (() => { 155pipes . delete ( id ); 156send ({ i, o :[ "end" , null ] }); 157}); 158}); 159return { pipe :id }; 160} 161 162type Opened = { ok :true ; value :object } | { ok :false ; error :unknown }; 163 164function keep ( result :Opened , extra :( value :object ) => object = () => ({})) { 165if ( ! result . ok ) return result ; 166const handle = ++ handled ; 167handles . set ( handle , result . value as Record < string , ( ...args :unknown []) => Promise < unknown >>); 168return { ok :true , value :{ handle, ...extra ( result . value ) } }; 169} 170 171async function dispatch ( 172i :number , 173m :string , 174a :readonly unknown [], 175cwd :string , 176context :Context , 177) { 178const env = new NodeExecutionEnv ({ cwd}); 179switch ( m ) { 180case "exec" :{ 181const [ command , options ] = a as [ string | string [], ShellExecOptions & { output :boolean }]; 182const { output :streamed , ...rest } = options ; 183return await env . exec ( command , { 184 ...rest , 185 ...( streamed ?{ onOutput :( text , _ , info ) => send ({ i, o :[ text , info ] }) } :{}), 186}, context ); 187} 188case "openBinaryReader" : 189return keep ( 190await env . openBinaryReader ( a [ 0 ] as string , a [ 1 ] as { noFollow ?:boolean }, context ), 191); 192case "openTextLineReader" : 193return keep ( await env . openTextLineReader ( a [ 0 ] as string , context )); 194case "openDirReader" : 195return keep ( await env . openDirReader ( a [ 0 ] as string , context )); 196case "fetch" :{ 197const [ url , options ] = a as [ string , FetchOptions ]; 198const lifetime = new AbortController (); 199const abort = () => lifetime . abort ( context . abortSignal ?. reason ); 200context . abortSignal ?. addEventListener ( "abort" , abort , { once :true }); 201if ( context . abortSignal ?. aborted ) abort (); 202let response :globalThis . Response ; 203try { 204response = await fetch ( url , { 205 ...options , 206body :options . body === undefined ?undefined :new Uint8Array ( options . body ), 207signal :lifetime . signal , 208}); 209} finally { 210context . abortSignal ?. removeEventListener ( "abort" , abort ); 211} 212const metadata = { 213status :response . status , 214statusText :response . statusText , 215headers :[ ...response . headers ], 216url :response . url , 217redirected :response . redirected , 218}; 219if ( ! response . body ) return metadata ; 220const reader = response . body . getReader (); 221const handle = ++ handled ; 222const close = async () => { 223handles . delete ( handle ); 224lifetime . abort (); 225await reader . cancel (). catch (() => {}); 226}; 227handles . set ( handle , { 228async read ( context :Context ) { 229const abort = () => lifetime . abort ( context . abortSignal ?. reason ); 230context . abortSignal ?. addEventListener ( "abort" , abort , { once :true }); 231if ( context . abortSignal ?. aborted ) abort (); 232try { 233const next = await reader . read (); 234if ( next . done ) handles . delete ( handle ); 235return next ; 236} catch ( error ) { 237await close (); 238throw error ; 239} finally { 240context . abortSignal ?. removeEventListener ( "abort" , abort ); 241} 242}, 243 close, 244} as Record < string , ( ...args :unknown []) => Promise < unknown >>); 245return { ...metadata , handle}; 246} 247case "spawn" :{ 248const [ argv , options ] = a as [ string [], SpawnOptions ]; 249const at = options . cwd === undefined ?cwd :resolve ( cwd , options . cwd ); 250// it outlives this call: berry kills it through its pipe 251return pipe ( i , await spawnHere ( argv , { ...options , cwd :at }, BACKGROUND_CONTEXT ), { 252group :options . group , 253}); 254} 255case "pty" :{ 256const [ argv , options ] = a as [ string [], PtyOptions ]; 257const at = options . cwd === undefined ?cwd :resolve ( cwd , options . cwd ); 258const pty = await ptyHere ( argv , { ...options , cwd :at }, BACKGROUND_CONTEXT ); 259return pipe ( i , { 260stdin :pty . writable , 261stdout :pty . readable , 262stderr :new ReadableStream ({ start :( controller ) => controller . close () }), 263exited :pty . exited , 264kill :pty . kill , 265resize :pty . resize , 266}); 267} 268case "connect" :{ 269const socket = await connectHere ( a [ 0 ] as string , a [ 1 ] as number , context ); 270return pipe ( i , { 271stdin :socket . writable , 272stdout :socket . readable , 273stderr :new ReadableStream ({ start :( controller ) => controller . close () }), 274// done once the far end closes it 275exited :Promise . resolve ( 0 ), 276kill :socket . close , 277}, { socket :true }); 278} 279case "pipe" :{ 280const [ id , operation , ...rest ] = a as [ 281number , 282"write" | "end" | "kill" | "resize" , 283 ...unknown [], 284]; 285const target = pipes . get ( id ); 286// already ended 287if ( target === undefined ) return ; 288if ( operation === "write" ) await target . stdin . write ( rest [ 0 ] as Uint8Array ); 289else if ( operation === "end" ) await target . stdin . close (); 290else if ( operation === "resize" ) target . resize ?.( rest [ 0 ] as number , rest [ 1 ] as number ); 291else target . kill ( rest [ 0 ] as "SIGTERM" | "SIGKILL" | undefined ); 292return ; 293} 294case "watch" : 295return keep ( 296await env . watch ( a [ 0 ] as never , ( change ) => send ({ i, o :change }), context ), 297( watcher ) => ({ mode :( watcher as { mode :string }). mode }), 298); 299case "handle" :{ 300const [ handle , method , ...rest ] = a as [ number , string , ...unknown []]; 301const target = handles . get ( handle ); 302// as a reader refuses use after it is closed, and takes another close as done 303if ( target === undefined ) { 304return method === "close" 305 ?undefined 306 :{ ok :false , error :new FileError ( "invalid" , `handle ${ handle } is closed` ) }; 307} 308if ( method === "close" ) handles . delete ( handle ); 309return await target [ method ]. call ( target , ...rest , context ); 310} 311} 312if ( ! PLAIN . has ( m )) throw new Error ( `No method ${ m } ` ); 313const method = env [ m as keyof NodeExecutionEnv ] as ( ...args :unknown []) => Promise < unknown >; 314return await method . call ( env , ...a , context ); 315} 316 317send ({ 318hello :{ 319protocol :PROTOCOL , 320platform :process . platform , 321home :homedir (), 322variables :Object . fromEntries ( 323Object . entries ( process . env ). filter (( entry ) :entry is[ string , string ] => 324entry [ 1 ] !== undefined 325), 326), 327}, 328}); 329 330for await ( const line of lines ( input . reader , input . buffered )) { 331if ( line === "" ) continue ; 332const request = decode ( line ) as Request ; 333if ( "c" in request ) { 334running . get ( request . c )?. abort (); 335continue ; 336} 337const { i, m, a, cwd= homedir () } = request ; 338const controller = new AbortController (); 339running . set ( i , controller ); 340const settled = dispatch ( i , m , a , cwd , withAbortSignal ( controller . signal , BACKGROUND_CONTEXT )) 341. then ( 342( r ) => send ({ i, r}), 343( error ) => send ({ i, e :error instanceof Error ?error . message :String ( error ) }), 344) 345. finally (() => { 346running . delete ( i ); 347settling . delete ( settled ); 348}); 349settling . add ( settled ); 350} 351 352// berry has gone: so does everything it started 353shuttingDown = true ; 354for ( const controller of running . values ()) controller . abort (); 355const owned = [ ...pipes . values ()]; 356for ( const { kill} of owned ) kill (); 357await settleWithin ( Promise . allSettled ( settling ), 1000 ); 358await settleWithin ( 359Promise . allSettled ([ 360 ...owned . map (( pipe ) => pipe . close ()), 361 ...[ ...handles . values ()]. map (( handle ) => handle . close ?. call ( handle , BACKGROUND_CONTEXT )), 362]), 3631000 , 364); 365await settleWithin ( output . close (), 1000 ); 366Deno . exit ( 0 );