char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
0853dde
main
1import type { Context } from "@earendil-works/chord" ; 2import type { Pty , PtyOptions } from "./environment.ts" ; 3 4const EXTENDED_STARTUPINFO_PRESENT = 0x00080000 ; 5const CREATE_UNICODE_ENVIRONMENT = 0x00000400 ; 6const STARTF_USESTDHANDLES = 0x00000100 ; 7const PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x20016n ; 8const INFINITE = 0xffffffff ; 9 10let kernel :ReturnType < typeof openKernel > | undefined ; 11 12function openKernel () { 13return Deno . dlopen ( "kernel32.dll" , { 14CreatePipe :{ parameters :[ "buffer" , "buffer" , "pointer" , "u32" ], result :"i32" }, 15CloseHandle :{ parameters :[ "pointer" ], result :"i32" }, 16GetLastError :{ parameters :[], result :"u32" }, 17CreatePseudoConsole :{ 18parameters :[ "u32" , "pointer" , "pointer" , "u32" , "buffer" ], 19result :"i32" , 20}, 21ResizePseudoConsole :{ parameters :[ "pointer" , "u32" ], result :"i32" }, 22// closing can wait for output to drain on Windows versions before 11 24H2 23ClosePseudoConsole :{ parameters :[ "pointer" ], result :"void" , nonblocking :true }, 24InitializeProcThreadAttributeList :{ 25parameters :[ "buffer" , "u32" , "u32" , "buffer" ], 26result :"i32" , 27}, 28UpdateProcThreadAttribute :{ 29parameters :[ "buffer" , "u32" , "usize" , "pointer" , "usize" , "pointer" , "pointer" ], 30result :"i32" , 31}, 32DeleteProcThreadAttributeList :{ parameters :[ "buffer" ], result :"void" }, 33CreateProcessW :{ 34parameters :[ 35"pointer" , 36"buffer" , 37"pointer" , 38"pointer" , 39"i32" , 40"u32" , 41"buffer" , 42"buffer" , 43"buffer" , 44"buffer" , 45], 46result :"i32" , 47}, 48WaitForSingleObject :{ parameters :[ "pointer" , "u32" ], result :"u32" , nonblocking :true }, 49GetExitCodeProcess :{ parameters :[ "pointer" , "buffer" ], result :"i32" }, 50TerminateProcess :{ parameters :[ "pointer" , "u32" ], result :"i32" }, 51ReadFile :{ 52parameters :[ "pointer" , "buffer" , "u32" , "buffer" , "pointer" ], 53result :"i32" , 54nonblocking :true , 55}, 56WriteFile :{ 57parameters :[ "pointer" , "buffer" , "u32" , "buffer" , "pointer" ], 58result :"i32" , 59nonblocking :true , 60}, 61}). symbols ; 62} 63 64function wide ( text :string ) :Uint16Array { 65const buffer = new Uint16Array ( text . length + 1 ); 66for ( let i = 0 ; i < text . length ; i ++ ) buffer [ i ] = text . charCodeAt ( i ); 67return buffer ; 68} 69 70function size ( cols :number , rows :number ) :number { 71if ( ! [ cols , rows ]. every (( n ) => Number . isInteger ( n ) && n > 0 && n <= 32767 )) { 72throw new Error ( "Terminal dimensions must be between 1 and 32767" ); 73} 74// COORD is two signed shorts, passed by value 75return ( rows << 16 ) | cols ; 76} 77 78/** A native ConPTY session on Windows 10 1809 or newer, with UTF-8 terminal I/O. */ 79export async function windowsPty ( 80argv :readonly string [], 81{ cwd, env, cols, rows} :PtyOptions , 82context :Context , 83) :Promise < Pty > { 84context . abortSignal ?. throwIfAborted (); 85if ( ! argv . length || [ ...argv , cwd ?? "" ]. some (( s ) => s . includes ( "\0" ))) { 86throw new Error ( "Expected a program and arguments without NUL characters" ); 87} 88const dimensions = size ( cols , rows ); 89const c = kernel ??= openKernel (); 90const fail = ( operation :string ) => 91new Error ( ` ${ operation } failed (Windows error ${ c . GetLastError ()} )` ); 92const pointer = ( handle :bigint ) => Deno . UnsafePointer . create ( handle ); 93const handles = new Set < bigint >(); 94const closeHandle = ( handle :bigint ) => { 95if ( handles . delete ( handle )) c . CloseHandle ( pointer ( handle )); 96}; 97const pipe = () => { 98const read = new BigUint64Array ( 1 ); 99const write = new BigUint64Array ( 1 ); 100if ( ! c . CreatePipe ( read , write , null , 0 )) throw fail ( "CreatePipe" ); 101handles . add ( read [ 0 ]); 102handles . add ( write [ 0 ]); 103return [ read [ 0 ], write [ 0 ]]; 104}; 105const console = new BigUint64Array ( 1 ); 106let input :bigint ; 107let output :bigint ; 108let process :bigint ; 109try { 110const [ inputRead , inputWrite ] = pipe (); 111const [ outputRead , outputWrite ] = pipe (); 112input = inputWrite ; 113output = outputRead ; 114const hr = c . CreatePseudoConsole ( 115dimensions , 116pointer ( inputRead ), 117pointer ( outputWrite ), 1180 , 119console , 120); 121if ( hr < 0 ) { 122throw new Error ( `CreatePseudoConsole failed (HRESULT 0x ${( hr >>> 0 ). toString ( 16 )} )` ); 123} 124 125const bytes = new BigUint64Array ( 1 ); 126c . InitializeProcThreadAttributeList ( null , 1 , 0 , bytes ); 127const attributes = new Uint8Array ( Number ( bytes [ 0 ])); 128if ( ! c . InitializeProcThreadAttributeList ( attributes , 1 , 0 , bytes )) { 129throw fail ( "InitializeProcThreadAttributeList" ); 130} 131try { 132if ( 133! c . UpdateProcThreadAttribute ( 134attributes , 1350 , 136PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE , 137pointer ( console [ 0 ]), 1388n , 139null , 140null , 141) 142) { 143throw fail ( "UpdateProcThreadAttribute" ); 144} 145// STARTUPINFOEXW and PROCESS_INFORMATION have the same layout on x64 and arm64 146const startup = new Uint8Array ( 112 ); 147const view = new DataView ( startup . buffer ); 148view . setUint32 ( 0 , startup . byteLength , true ); 149// explicit null standard handles bind to ConPTY instead of inheriting the worker's protocol pipes 150view . setUint32 ( 60 , STARTF_USESTDHANDLES , true ); 151view . setBigUint64 ( 152104 , 153BigInt ( Deno . UnsafePointer . value ( Deno . UnsafePointer . of ( attributes ))), 154true , 155); 156const info = new BigUint64Array ( 3 ); 157const variables = new Map < string , [ string , string ]>(); 158for ( 159const [ key , value ] of Object . entries ( Deno . env . toObject ()). concat ( Object . entries ( env ?? {})) 160) { 161if ( key . includes ( "\0" ) || value . includes ( "\0" )) { 162throw new Error ( "NUL in process environment" ); 163} 164variables . set ( key . toUpperCase (), [ key , value ]); 165} 166const block = [ ...variables ]. sort (([ a ], [ b ]) => a < b ?- 1 :a > b ?1 :0 ) 167. map (([, [ key , value ]]) => ` ${ key } = ${ value } ` ). join ( "\0" ) + "\0" ; 168// preserve quotes and their preceding backslashes through Windows command-line parsing 169const command = argv . map (( arg ) => 170`" ${ arg . replace ( / (\\*)" / g , '$1$1\\"' ). replace ( / (\\+)$ / , "$1$1" )} "` 171). join ( " " ); 172if ( 173! c . CreateProcessW ( 174null , 175wide ( command ), 176null , 177null , 1780 , 179EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT , 180wide ( block ), 181cwd === undefined ?null :wide ( cwd ), 182startup , 183info , 184) 185) throw fail ( `Could not start ${ argv [ 0 ]} ` ); 186process = info [ 0 ]; 187handles . add ( process ); 188c . CloseHandle ( pointer ( info [ 1 ])); 189} finally { 190c . DeleteProcThreadAttributeList ( attributes ); 191} 192closeHandle ( inputRead ); 193closeHandle ( outputWrite ); 194} catch ( error ) { 195for ( const handle of handles ) closeHandle ( handle ); 196if ( console [ 0 ]) await c . ClosePseudoConsole ( pointer ( console [ 0 ])); 197throw error ; 198} 199 200let closing :Promise < void > | undefined ; 201let writing = Promise . resolve (); 202let killed = false ; 203const close = () => { 204if ( closing ) return ; 205closing = c . ClosePseudoConsole ( pointer ( console [ 0 ])) 206. then (() => writing . catch (() => {})) 207. then (() => closeHandle ( input )); 208}; 209const kill = () => { 210if ( closing ) return ; 211killed = true ; 212// a process aborted before its console initialization can miss ConPTY's close notification 213c . TerminateProcess ( pointer ( process ), 1 ); 214close (); 215}; 216context . abortSignal ?. addEventListener ( "abort" , kill , { once :true }); 217if ( context . abortSignal ?. aborted ) kill (); 218const exited = c . WaitForSingleObject ( pointer ( process ), INFINITE ). then (( status ) => { 219const code = new Uint32Array ( 1 ); 220return status === 0 && c . GetExitCodeProcess ( pointer ( process ), code ) && ! killed ?code [ 0 ] :null ; 221}). finally (() => { 222closeHandle ( process ); 223context . abortSignal ?. removeEventListener ( "abort" , kill ); 224close (); 225}); 226 227let ended = false ; 228let cancelled = false ; 229let reading = Promise . resolve (); 230const read = async () => { 231if ( ended ) return ; 232const buffer = new Uint8Array ( 65536 ); 233const count = new Uint32Array ( 1 ); 234if ( await c . ReadFile ( pointer ( output ), buffer , buffer . length , count , null ) && count [ 0 ]) { 235return buffer . subarray ( 0 , count [ 0 ]); 236} 237ended = true ; 238closeHandle ( output ); 239await closing ; 240}; 241const readable = new ReadableStream < Uint8Array >({ 242pull ( controller ) { 243reading = read (). then (( chunk ) => { 244if ( cancelled ) return ; 245if ( chunk ) controller . enqueue ( chunk ); 246else controller . close (); 247}); 248return reading ; 249}, 250async cancel () { 251cancelled = true ; 252kill (); 253// keep draining for ClosePseudoConsole even after the consumer has gone away 254await reading ; 255while ( ! ended ) await read (); 256}, 257}, { highWaterMark :0 }); 258const writable = new WritableStream < Uint8Array >({ 259write ( chunk ) { 260writing = ( async () => { 261const count = new Uint32Array ( 1 ); 262for ( let at = 0 ; at < chunk . length ;) { 263if ( closing ) throw new Error ( "The terminal is closed" ); 264const rest = chunk . subarray ( at ); 265if ( ! await c . WriteFile ( pointer ( input ), rest , rest . length , count , null ) || ! count [ 0 ]) { 266throw new Error ( "Could not write to the terminal" ); 267} 268at += count [ 0 ]; 269} 270})(); 271return writing ; 272}, 273close :() => closeHandle ( input ), 274abort :kill , 275}); 276return { 277 readable, 278 writable, 279 exited, 280 kill, 281resize ( cols , rows ) { 282if ( closing ) return ; 283const hr = c . ResizePseudoConsole ( pointer ( console [ 0 ]), size ( cols , rows )); 284if ( hr < 0 ) { 285throw new Error ( `ResizePseudoConsole failed (HRESULT 0x ${( hr >>> 0 ). toString ( 16 )} )` ); 286} 287}, 288}; 289}