From e94939a562126862d13864a0cd6c74b831bf4c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 17 Apr 2026 02:36:53 -0400 Subject: [PATCH] test(connector-id): migrate fixtures and assertions to pi-mono ids MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces legacy adapter ids (codex_local / claude_local / gemini_local / claude_compatible) across ~42 test files with pi-mono equivalents: - codex_local → openai-codex - claude_local → claude-pro - gemini_local → gemini-cli - claude_compatible → anthropic Hand-rewrites where fixture semantics changed: - connector-matching: TASK_TYPE_PREFERENCE ordering rebased (claude-pro first for code/content/management; google first for research) - connector-model-policy: getModels(provider) from pi-ai instead of the deleted adapter.module.models array - session-management + edge-case-session: adapter exemption removed; rotation now uniform under pi-mono - ipc-core-admin-handlers: CSV compat model replaced with pi-ai anthropic - connectors.spec: lookup by openai-codex id (catalog now starts with claude-pro) - onboarding-model-pool: listProviderModels(provider) helper; label text changed to "ChatGPT Plus/Pro (Codex)" - database integration: added vault insecure-fallback env + electron mock - command-paths: uses new @main/connector-helpers normalizeCommandValue - pi-mono-migrations: added `as string` casts for 3 negative-existence assertions verifying legacy ids were removed Drops: - pnpm-workspace.yaml packages: [] (was "packages/*"; directory empty) - obsolete CONNECTOR_STRENGTHS import; .module.models probing Verification: - pnpm typecheck → 0 errors - pnpm lint → 0 errors / 0 warnings - pnpm build:verify → 0 errors / 0 warnings - pnpm test:unit → 508/508 pass - pnpm test:integration → 96/96 pass - All 6 adapter hard gates pass (NOT FOUND / 0 matches / empty) - pi-ai / pi-agent-core / pi-coding-agent all resolved @0.67.6 --- .../doom-overlay/doom/build/doom.js | 21 +++ .../doom-overlay/doom/build/doom.wasm | Bin 0 -> 380169 bytes pnpm-workspace.yaml | 3 +- src/main/agent-role-helpers.ts | 30 +++- src/main/agent-runtime/approval-hook.ts | 134 +++++++++++++++ src/main/agent-runtime/index.ts | 4 + .../agent-runtime/sqlite-session-store.ts | 65 ++++++++ src/main/credentials/credential-store.ts | 122 ++++++++++++++ src/main/credentials/index.ts | 4 + src/main/credentials/types.ts | 30 ++++ src/main/database-stale-recovery.ts | 70 ++++++++ src/main/database/entity-crud.ts | 4 +- src/main/database/snapshot.ts | 2 +- src/main/model-resolver.ts | 91 +++++++++++ src/main/resume-interrupted-tasks.ts | 65 ++++++++ src/main/service-graph-orchestration-deps.ts | 65 ++++++++ src/main/service-graph-pi-runtime.ts | 58 +++++++ src/main/suppress-warnings.ts | 25 +++ src/main/worker/log-redactor.ts | 81 ++++++++++ .../approval-inspector/SchedulePicker.tsx | 2 +- src/renderer/components/ui/AttentionRow.tsx | 2 +- src/renderer/components/ui/feedback.tsx | 2 +- src/shared/contracts/schemas-agent-event.ts | 21 +++ src/shared/contracts/schemas-credentials.ts | 89 ++++++++++ src/shared/contracts/schemas-oauth.ts | 60 +++++++ src/shared/types/records-core.ts | 3 + .../integration/agent-model-switching.spec.ts | 6 +- .../api-recursive-org-execution.spec.ts | 22 +-- tests/integration/api-run-events.spec.ts | 4 +- .../approval-decisions.spec.ts | 2 +- .../automation-messages.spec.ts | 12 +- .../autonomous-chain/crash-recovery.spec.ts | 4 +- .../autonomous-chain/run-failure.spec.ts | 2 +- .../autonomous-chain/shared-setup.ts | 2 +- .../connector-model-enforcement.spec.ts | 14 +- tests/integration/cross-subsystem.spec.ts | 8 +- tests/integration/database.spec.ts | 26 ++- .../document-run-provenance.spec.ts | 6 +- .../integration/onboarding-model-pool.spec.ts | 105 +++++++----- tests/integration/pi-mono-migrations.spec.ts | 153 ++++++++++++++++++ tests/integration/process-recovery.spec.ts | 6 +- .../skill-aware-assignment.spec.ts | 12 +- .../integration/thread-wait-heartbeat.spec.ts | 8 +- tests/unit/agent-budget-policy-sync.test.ts | 6 +- tests/unit/agent-hire-routing.test.ts | 8 +- tests/unit/agent-permissions.test.ts | 18 +-- tests/unit/agent-schema-validation.test.ts | 2 +- tests/unit/approval-hire-routing.test.ts | 8 +- tests/unit/approval-hook.test.ts | 2 +- ...approval-review-feedback-writeback.test.ts | 4 +- tests/unit/atomic-checkout.test.ts | 20 +-- tests/unit/capability-registry.test.ts | 4 +- tests/unit/command-paths.test.ts | 42 +---- tests/unit/company-mode-switch.test.ts | 6 +- tests/unit/connector-matching.test.ts | 39 +++-- tests/unit/connector-model-policy.test.ts | 67 ++++---- tests/unit/connectors.spec.ts | 10 +- tests/unit/contracts.test.ts | 2 +- tests/unit/credential-resolver.test.ts | 146 +++++++++++++++++ tests/unit/credential-store.test.ts | 80 +++++++++ tests/unit/credentials-schemas.test.ts | 143 ++++++++++++++++ tests/unit/dashboard-charts.test.ts | 6 +- tests/unit/database.test.ts | 32 ++-- tests/unit/edge-case-budget.test.ts | 18 +-- tests/unit/edge-case-concurrent.test.ts | 28 ++-- tests/unit/edge-case-session.test.ts | 54 +++---- tests/unit/heartbeat-coalescing.test.ts | 4 +- tests/unit/ipc-agent-graph-handlers.test.ts | 8 +- tests/unit/ipc-core-admin-handlers.test.ts | 28 +++- tests/unit/log-redactor.test.ts | 61 +++++++ tests/unit/message-dispatch.test.ts | 12 +- tests/unit/message-search.test.ts | 4 +- tests/unit/oauth-login-service.test.ts | 64 ++++++++ .../prompt-manager-classification.test.ts | 4 +- tests/unit/session-management.test.ts | 115 ++++++------- tests/unit/skill-injection.test.ts | 6 +- tests/unit/skill-types.test.ts | 39 +++-- tests/unit/sqlite-session-store.test.ts | 80 +++++++++ tests/unit/store.test.ts | 4 +- tests/unit/wakeup-lifecycle.test.ts | 4 +- 80 files changed, 2210 insertions(+), 413 deletions(-) create mode 100644 pi-mono-reference/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.js create mode 100755 pi-mono-reference/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.wasm create mode 100644 src/main/agent-runtime/approval-hook.ts create mode 100644 src/main/agent-runtime/index.ts create mode 100644 src/main/agent-runtime/sqlite-session-store.ts create mode 100644 src/main/credentials/credential-store.ts create mode 100644 src/main/credentials/index.ts create mode 100644 src/main/credentials/types.ts create mode 100644 src/main/database-stale-recovery.ts create mode 100644 src/main/model-resolver.ts create mode 100644 src/main/resume-interrupted-tasks.ts create mode 100644 src/main/service-graph-orchestration-deps.ts create mode 100644 src/main/service-graph-pi-runtime.ts create mode 100644 src/main/suppress-warnings.ts create mode 100644 src/main/worker/log-redactor.ts create mode 100644 src/shared/contracts/schemas-agent-event.ts create mode 100644 src/shared/contracts/schemas-credentials.ts create mode 100644 src/shared/contracts/schemas-oauth.ts create mode 100644 tests/integration/pi-mono-migrations.spec.ts create mode 100644 tests/unit/credential-resolver.test.ts create mode 100644 tests/unit/credential-store.test.ts create mode 100644 tests/unit/credentials-schemas.test.ts create mode 100644 tests/unit/log-redactor.test.ts create mode 100644 tests/unit/oauth-login-service.test.ts create mode 100644 tests/unit/sqlite-session-store.test.ts diff --git a/pi-mono-reference/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.js b/pi-mono-reference/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.js new file mode 100644 index 00000000..e2221dc2 --- /dev/null +++ b/pi-mono-reference/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.js @@ -0,0 +1,21 @@ +var createDoomModule = (() => { + var _scriptName = typeof document != 'undefined' ? document.currentScript?.src : undefined; + if (typeof __filename != 'undefined') _scriptName = _scriptName || __filename; + return ( +async function(moduleArg = {}) { + var moduleRtn; + +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WORKER=false;var ENVIRONMENT_IS_NODE=true;if(ENVIRONMENT_IS_NODE){}var moduleOverrides={...Module};var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAP64,HEAPU64,HEAPF64;var runtimeInitialized=false;var isFileURI=filename=>filename.startsWith("file://");function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b);Module["HEAP64"]=HEAP64=new BigInt64Array(b);Module["HEAPU64"]=HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();TTY.init();wasmExports["__wasm_call_ctors"]();FS.ignorePermissions=false}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}var runDependencies=0;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("doom.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){return{env:wasmImports,wasi_snapshot_preview1:wasmImports}}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["memory"];updateMemoryViews();removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(mod,inst)=>{receiveInstance(mod,inst);resolve(mod.exports)})})}wasmBinaryFile??=findWasmBinary();try{var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}catch(e){readyPromiseReject(e);return Promise.reject(e)}}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.unshift(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.unshift(cb);function getValue(ptr,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":return HEAP8[ptr];case"i8":return HEAP8[ptr];case"i16":return HEAP16[ptr>>1];case"i32":return HEAP32[ptr>>2];case"i64":return HEAP64[ptr>>3];case"float":return HEAPF32[ptr>>2];case"double":return HEAPF64[ptr>>3];case"*":return HEAPU32[ptr>>2];default:abort(`invalid type for getValue: ${type}`)}}var noExitRuntime=Module["noExitRuntime"]||true;function setValue(ptr,value,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":HEAP8[ptr]=value;break;case"i8":HEAP8[ptr]=value;break;case"i16":HEAP16[ptr>>1]=value;break;case"i32":HEAP32[ptr>>2]=value;break;case"i64":HEAP64[ptr>>3]=BigInt(value);break;case"float":HEAPF32[ptr>>2]=value;break;case"double":HEAPF64[ptr>>3]=value;break;case"*":HEAPU32[ptr>>2]=value;break;default:abort(`invalid type for setValue: ${type}`)}}var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\/]+|\/)\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(ENVIRONMENT_IS_NODE){var nodeCrypto=require("crypto");return view=>nodeCrypto.randomFillSync(view)}return view=>crypto.getRandomValues(view)};var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var mmapAlloc=size=>{abort()};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[".","..",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=Module["preloadPlugins"]||[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url).then(processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class{name="ErrnoError";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+"/"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name==="."||name===".."){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+="/";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path=="object"){node=path}else{isDirPath=path.endsWith("/");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module["logReadFiles"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){throw new Error(`Invalid encoding type "${opts.encoding}"`)}var ret;var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){ret=UTF8ArrayToString(buf)}else if(opts.encoding==="binary"){ret=buf}FS.close(stream);return ret},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){var buf=new Uint8Array(lengthBytesUTF8(data)+1);var actualNumBytes=stringToUTF8Array(data,buf,0,buf.length);FS.write(stream,buf,0,actualNumBytes,undefined,opts.canOwn)}else if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{throw new Error("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+"/"+path},writeStat(buf,stat){HEAP32[buf>>2]=stat.dev;HEAP32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAP32[buf+12>>2]=stat.uid;HEAP32[buf+16>>2]=stat.gid;HEAP32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAP32[buf+4>>2]=stats.bsize;HEAP32[buf+40>>2]=stats.bsize;HEAP32[buf+8>>2]=stats.blocks;HEAP32[buf+12>>2]=stats.bfree;HEAP32[buf+16>>2]=stats.bavail;HEAP32[buf+20>>2]=stats.files;HEAP32[buf+24>>2]=stats.ffree;HEAP32[buf+28>>2]=stats.fsid;HEAP32[buf+44>>2]=stats.flags;HEAP32[buf+36>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_mkdirat(dirfd,path,mode){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);FS.mkdir(path,mode,0);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_renameat(olddirfd,oldpath,newdirfd,newpath){try{oldpath=SYSCALLS.getStr(oldpath);newpath=SYSCALLS.getStr(newpath);oldpath=SYSCALLS.calculateAt(olddirfd,oldpath);newpath=SYSCALLS.calculateAt(newdirfd,newpath);FS.rename(oldpath,newpath);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_rmdir(path){try{path=SYSCALLS.getStr(path);FS.rmdir(path);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_unlinkat(dirfd,path,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(flags===0){FS.unlink(path)}else if(flags===512){FS.rmdir(path)}else{abort("Invalid flags passed to unlinkat")}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __emscripten_system=command=>{if(ENVIRONMENT_IS_NODE){if(!command)return 1;var cmdstr=UTF8ToString(command);if(!cmdstr.length)return 0;var cp=require("child_process");var ret=cp.spawnSync(cmdstr,[],{shell:true,stdio:"inherit"});var _W_EXITCODE=(ret,sig)=>ret<<8|sig;if(ret.status===null){var signalToNumber=sig=>{switch(sig){case"SIGHUP":return 1;case"SIGQUIT":return 3;case"SIGFPE":return 8;case"SIGKILL":return 9;case"SIGALRM":return 14;case"SIGTERM":return 15;default:return 2}};return _W_EXITCODE(0,signalToNumber(ret.signal))}return _W_EXITCODE(ret.status,0)}if(!command)return 0;return-52};var _emscripten_get_now=()=>performance.now();var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};var FS_createPath=FS.createPath;var FS_unlink=path=>FS.unlink(path);var FS_createLazyFile=FS.createLazyFile;var FS_createDevice=FS.createDevice;FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();Module["FS_createPath"]=FS.createPath;Module["FS_createDataFile"]=FS.createDataFile;Module["FS_createPreloadedFile"]=FS.createPreloadedFile;Module["FS_unlink"]=FS.unlink;Module["FS_createLazyFile"]=FS.createLazyFile;Module["FS_createDevice"]=FS.createDevice;MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack="";var wasmImports={__syscall_fcntl64:___syscall_fcntl64,__syscall_ioctl:___syscall_ioctl,__syscall_mkdirat:___syscall_mkdirat,__syscall_openat:___syscall_openat,__syscall_renameat:___syscall_renameat,__syscall_rmdir:___syscall_rmdir,__syscall_unlinkat:___syscall_unlinkat,_emscripten_system:__emscripten_system,emscripten_get_now:_emscripten_get_now,emscripten_resize_heap:_emscripten_resize_heap,exit:_exit,fd_close:_fd_close,fd_read:_fd_read,fd_seek:_fd_seek,fd_write:_fd_write};var wasmExports=await createWasm();var ___wasm_call_ctors=wasmExports["__wasm_call_ctors"];var _free=Module["_free"]=wasmExports["free"];var _malloc=Module["_malloc"]=wasmExports["malloc"];var _doomgeneric_Tick=Module["_doomgeneric_Tick"]=wasmExports["doomgeneric_Tick"];var _doomgeneric_Create=Module["_doomgeneric_Create"]=wasmExports["doomgeneric_Create"];var _DG_GetFrameBuffer=Module["_DG_GetFrameBuffer"]=wasmExports["DG_GetFrameBuffer"];var _DG_GetScreenWidth=Module["_DG_GetScreenWidth"]=wasmExports["DG_GetScreenWidth"];var _DG_GetScreenHeight=Module["_DG_GetScreenHeight"]=wasmExports["DG_GetScreenHeight"];var _DG_PushKeyEvent=Module["_DG_PushKeyEvent"]=wasmExports["DG_PushKeyEvent"];var __emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];var __emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];var _emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"];Module["addRunDependency"]=addRunDependency;Module["removeRunDependency"]=removeRunDependency;Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["setValue"]=setValue;Module["getValue"]=getValue;Module["FS_createPreloadedFile"]=FS_createPreloadedFile;Module["FS_unlink"]=FS_unlink;Module["FS_createPath"]=FS_createPath;Module["FS_createDevice"]=FS_createDevice;Module["FS"]=FS;Module["FS_createDataFile"]=FS_createDataFile;Module["FS_createLazyFile"]=FS_createLazyFile;function run(){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise; + + + return moduleRtn; +} +); +})(); +if (typeof exports === 'object' && typeof module === 'object') { + module.exports = createDoomModule; + // This default export looks redundant, but it allows TS to import this + // commonjs style module. + module.exports.default = createDoomModule; +} else if (typeof define === 'function' && define['amd']) + define([], () => createDoomModule); diff --git a/pi-mono-reference/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.wasm b/pi-mono-reference/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.wasm new file mode 100755 index 0000000000000000000000000000000000000000..fb99ae7cefd8fcc156edd27fe79b9498c64821a4 GIT binary patch literal 380169 zcmd?S51gGxRsa9|x%ckfy}QY6jWjluK6|Mew6syN)`DL%Na@E#g$Mz*7%@u4Rigxqx@g3R1)>ItUt{xof6mN2 z_qlg>`!DLt@6WV%pP6}P&YU@O=FFKhXXZ&>b=QV8Ns{!h)6PxlmgJ^v3xCor>6V+4 z4ksR$UwAVQ4juJVy{&k zFiHAaK;3KMkjJW1nQRMyLC12alI*pLc*x|6l@ZLS-LNEAO&BTWAK8}IQj(O{W^c%s z=j(32r?&1jw{1=m15hcl$@&xb{U`Bm%J z-+0HJ>u!J5=A^0Qb9%|IzWMge>o0qLQd9c!Uixi!+`M^xQhU}9z3Q&pZoKRESKWEn zt#@p`@y<=_?zwH<4}Ej<)wkUELz`~fye?@jkzmp6gy|F?> zh_-p%24CV?mGn*P?z-(q*4=pPx>w!F%iqi&$@!NhqeS#?h)9|a4GpFByk`H>JZTKo zlRQt-#!!+b=~z-%o;)X8o+Pz;mNn{WTCc;w#Px@Vg%lZ%@jSKkJW=+>@X$~~iF!TF z^j!Eat0rl5gnwz;i>#o0t(Fe+s*xsHEz4^Bvf3CZWxNvLTDm+O*PTt#!LuqseG>Gw-;dHS{yt zvc`sW8}8Wj!&$P_p`B?t1x28P%jHzyy;zvjjpZ@V3C zTX*y38()3*?Kf|}?T*`T-2AE^Side!+PB%Wu1RGWptbt4ZI@ zfMH0DpY!6YZoF#U=9g@OJzsS9t6#ltQ_=}(|KnyVy#0G`yJhpO$#c&2`Fz^d>u&qO zTQ?^cEhq71ci(mEb?bik%6rz`zB&22uj%9L&9Az7l972A4Oo}_UVh%fm%b^V%rB@g z{#{l~vgPKxH!*NFC%>N$|7?~fE^YmCy7Gc&ul$yk=U-CXyZX7`_)Ql-@2y!kdtJ6Q zdqZ|#_U3FV+n)V+_F#5@_L20X>1_Iz?Dg5U?2Xw2*-vC|%HEd!M!G-!o%AQt52QQN zpG7I0N`tI~s(}&V`rN5N!PJcQ5#q>Std()Zp*U}HBKb^iK{n_;A z(w|R%A$?zZApM#2!|AT{L+Nz7FMT+DTl%r|H`CurA4xx+{&spOJ(%8j%MbqOYhU?) z|F7@+f4~2xSN*`v>t229Z4;C0H{5aO#!Yu^zWbgZ`r#khmcBXtm-Hm8@QL&f(?3cN zr=LoXq<@-zCjImDFVgw+uhL`b=hBC>ec1=H_h-M6?aw}xeK1@3x$JMUzs}BN-Tc1% z{`|XtDt}-8>-qch{rPD2fw7S{?OFCnensNCZ&~TO53al{OWctM33h7=9wrE*DY&2D zH!5&cX@4@jI!lPP?$6V7%X1PpolbU=i;^O7Bu}M0-=0pmVfUp^Ds3vAXz$2`DNKn1DWO*WTH7Q8^|=bAXELaZ0$g%wUYygH!jQ8 z4CGleh|$nxSuv2O7|heWENc(sX%FTZhQj@M29TLfCoW1_&raHHcjCyfNT0a5H#XWw zv-91l&ya9MlC&Oaq+?rL)72cnR%fG~)OA0t{_E7PUR9)ShmSO_UDe5Y*=n7%u=j;t zq#LiSK^hfmxKl^0bbfVh3&qn-b^d0XinM6BDT8(Gs#Za7ug;pC+O=fLEK{aTSyAg{ z+E`oblI1No{inl4qsS-S{x>$LqWucjzFSS%{inljPwEo4{nNwGOOjOrSqsSbWu8>- zJwQ>N0l&TsdcSuOXrqfl>(GITCxviOtFt~tIwLo%qE*S&tHxn{pQBEcw!+#FMBSEc z>f~-~z&tPNZZ^ASRf__?D~1XI88RTbkXJQL4Dr2 z-~A!l9X#tcA6I@!ZuqQ`#l4@ zu>@|IB^0}=GxR-U#=q)RHIf{3a)p;Wsd9vFuNRgd?3EvUt{I~-yI@#;p8e7Zk)l~3 z5)(K3FLhGgPyQ|((cYS?HPzH+O2+L45{*q4JV3~Sr&uGj`pb(XNxX@g^Zs0OsLtcyw!mUprOd{L}M{rw= zleQ$|A7trp=(iCjCqQ0HQ*!rJ_qc3xXVlGoF`F1)mW%hsTy672r-^_WA1ZhxPpl$R_V9yj!tDd%$7Yv(7_iehwvzwK8r9+tOXF*zPL8psSm|^&Lr2XFX%Nl_t(P{ z?hOxXaO=H+Q^lD^8D*03EN6K3GIumqI%)f7a*0hn;Pi=W;5X076P+3olljG6N=qdj z+i%bH7F3W8Dh=P(kOd^YBL_32L$1jdp;7Q)jJXF+Xz~SO-o8JjvTn*aN!tg=M`1OY zDa1DiSpbLTM$RH_{d8WEAQK7P+~110V@dm3=87X0L6j13$KTWfO-(X&xBGz>xumFV zD3Xbmd(L`yd}xC^Z232IOoASz3_mp=@x$Qiq>~I7(7bk)uvNgwkW4~@w9^!b~evzBDI!Qh!M?8nfC=o&0AZ)j<>+mW|e0)eNpqFOKHOU)Q{LOq706^6k`+g$IK;*38NY2^wI#Zh8k===x6Lj{hzE zm_Iic#{p(u(X1E!Jp5zaLs6i4!Ad`?kLu8a&dEHl!=PtIO37clC~02<{*QUeWC{S@ z)2EYqnrE^9Xm%vw)1zGI<)!E**;5jZQ1f~tS4jtG>}p218eQ3Gc(T>P@JC=*%z2D(in%?~>e zVwi|Y@{JNbvk$Z;+>DhgvTJ#zNWrQB!4{w;?qC@@RE8ccLmHC8zXcg$ONa8cv?3Lm zc=}d@M`$7HpOqd(`Efa(*JF0fe@ z3H+UHO<^6x@GhQ;?+DNBreTCP&eJ| z9KhOFw$u+rssVt|gStgCglTj!9Kvv3G2+AG=Tby9C88ieq`0O5dS7^6kucL-j3hVZ z))MNHC%jmp7hhd@@zvqQMSAgdl^0(ZUOZnfzOC}&+ro<%>&2Co7gvTCFV~CzS$Xk4 z!;4$=VqN9My6|GNUff-IakrKUw5U<`tW9n0^UyY!Ylggr*FG=Gra`_FmH#48nR)Y? zC@^k-Nn5CPro#Q=bqL^$VG&piHa+OIf<{n7uBvTOlHn5c3c@SaJ82$}x7BIDhje}B zHTWSk!A1vtg_+qne14avI z6J(BFK1zTL3W63mDRel#JxIW}K*@xxkdQcHZD?PkwHU-1We91->yCe=iQ;}GWA(AG zG)-whTltd~{aKevYjZhG8W+u=RQ3|e9F4Fl<4r-`bIg*+B`H{n_ zWCb9seqg)?osU^MtC?p{7CB}HRZnER=C+J$jv+k3&NPj7{gSiNU_OQd*@cJM?rQ|f5_aeCGlt-k$xCl`?C1=Q>3qK zZ$&j3H@&pXrvc=_QPs-zmb;UpjKlUtf}(%94HbjA%~yNxRg4J2S|Ml<2xb9ay1Ady z2w&CSlLKoWb#3i6G$u48bL0-~$2@*sL z${rmdG(t~gqkhKw_BX3IdI5@%=dt7^tE3Jidr}hT{y^`ql0vur*IQ1;(@oi=@VzH?!Lw>g7VHvv7~)lXpB)Fpu4JXt(krjX0Jd~< z^yA3SGUcpFw>>FcTJ8VGGVQ$IcmO!=-bnAg_YZsGQI!U69L#z@Lq^n*S#4iv))beP zGbS&UM>FQ18xKFpj444R1tt<=dI2F|w^zrvyDM)+@MEW%F4bR1(Mim_ePUX4ansp) z*L?_C_MW4Od*2ZxtV_jC^NflQ2Yh?{w5I~sr%FcqE=cKuE$)|2l?q96zT5wmlJ2l2 z0{nc&WhwD0MEWvBO1)~4yT3rU67^c+erQLcndlSAywwtldE@NjItHClC74&X{D6w6 zx4%CUY^KsM5$yOOX6!?vPzQoAjKLZMv!4hCNXM)5*Siy^n#_EV?GvA272W<))_$pm zFjTi$RTE5w(-w?^!BHp~i2?!w9%afBW~yVzz;WYAzE-#^sxC7OA1TsHQX(vSy%_i^ zi5_3FlWHr2+~lB1E7nf{#hp2mBU)Rk47H}LtWt-EPd+Ac*@-+f%EB9ym0E<0fA=Ky zpgENFELKRUNt+%;BPkLgnTd8mj>23KS&)vy; z*dSga{vAwwz6Bj7HXqGWbOh_fTXo}~XYVU`>2g(w&T2P~^ zW$0_)n<@ppO@lIo)K)*DtdB2ES5)Q>&5(szE!_vve=;}vQgCHX-R$mLvvC#Tlrai( zpTU|7u7MC@zj3QjTvn6i6rsqh#xfOytCc|wxf&%$>?n0ajLa(8IP&78UF0H;A=J-Y zs9AujT{SNGkxq;!TBo-}I1h_jAUqJHix_}x002g%t3-ho>mCV)B9q3Etqn6ohdH8< zCgmSBx7}Dt3#{GyH1^DbW+ABr+kV}~BTFumqq1=tV$eGwP`e6QA*JcWubR?yCCKMj zZ4_>l^g5iBYN-~$4pnTDhU@aHAi-W=Cshf@Y^|ZT3`dY8t+7r<4fbYDh2XBZ$P$ut z#uHEUw$tG4zNQ%(8GI$340xT~y+55?mS<_A3G@*1R_&L#Hq8^#iB4<^dtFDBcKyJ! z?%VxwdccJC);NWvGRyjfsAy4NPWeF7o%8{$2R@KaOiZ{OD{+8~ zq8$6w?X)Ri90Iv{QwNKT&M6XBlYaRrBN)pdSkg57A}`;nES(AKixNc*O26*ANXRq` zy2t-qJzZzl;C%NEB{wbj*4f1U>$_6o7qYjj_K8Q1RjN+0Z&}rKtD4E0%Ck_pONE`! zV)dIEF7+bT&w9D6Kke0zy{fN%w(KhPAFtG3HeE}GB1f1}e;(_vQ0L#O{Sp!M?_*IL zXDAH$sx@^vgp3IRMh!hEnybxfF-^t%%A@{0_GtRYg?iLPWn-UYl%2DhgYY5RQ1l(& z6=LvKO`R2@480oN7-CX#jX{9OI!wHpZiH!vuM>-F^w0fxW1=$zD;o<9a&;cKx>H}s z*U~2gSC2)mmertu*6_t`jg+O??hg_zf5VU^)hH)Z3~bF%_ktkzbZa}!OE!Qt_|=a#`0Gm! zezl7|dxn02(~%$gGM@vTR%Eu`_nvj~kkh|ZlU2lW_s2}G!#7~&<{oJ+if&;tFBXFK z)VhSc(r`DFW6v%m9`Z^n1&?$Vkv4k}CM$eiaJB&L4!luYyNC-9vjRRmVgQJL%v@vKoXJ3;dE6&-Z9c+&f(ZY#&n+heE9OoS z*rNVD5d+6#U_J(p#K2q(9EyS27}y^JGcnMWcab2U;os0WmJSjuti-L=$bH>z_caLV z?Ic=QIi{+S9iJIS+Rp#0%^|>*>XD&r*n04oC65n#5;4)^j z{vMDh`|f~YWbS{ny&&!-Zr)_40PQ1Yxa^OC-7zp711EvrPZc320=z;XPTT9ghR4_D zekF+j+xwZllT=hQB+eeS@*yKWk~$~`q``UtuYsvFzM*NmRe z5Z_UrI1uT`Q>*V(9o3lkiW#FP4L4E9GG6 zUO81xb>24GayyBdFVQX`C48m{Shw#cU>yhwIR-8qokJA%3+i{wT+-!zbW-kd$% z(M57E=^EK#n!=dw50t9`SR6)U)vwXt?Y65UT_bANFE-5T+Lt%!%RLEi zDEq#}vP;4&0($>ac^3zLsFGch?&lsTCEfr2vSxMFe{dj^f8%D${L18~@Sbf!L^El6 znrs?S?Q>)mn5)3j8-5y-nO0{-rj-~YF}SRfcJLk% zh))Pq!z}l`J^=v}00$U|2^+tNf>1{Q7hf!t*+6?zac!;c!9>GDT$*@N_5CPxa(ih+ zmv~7LhiZ((i*_Cen^}l*wB0DB*P1P8z2Yj0w&2-F^n+rgGvz}_0_qzCJ0a!9Y|%!p zN`B|EVT__>5@|8vgRwV9njnryM1+J~K*%WFKa~}ljZJ(;s*@nxsr+!I#}`Zl9&O+K z0nKr$H098JAQ4hZX?&Y!%3S56ICT5EdE_F9Tbi zU1LXl*jyLDE8IW*KB?{3rva=|Z3*D>G3fKnESOLOC;Iy}a1hkpr+3g63c*Z41v8qbH3S~e6XSYMRzn=?`FOAxabhTSd8Ya;qiUr5dzhs-D;2ZSv$VLlpd6r2(Q3j1_^j95P$_*LI%3}IwF!%itX zy;4KFzkuaRGEa64nezaw*E-e`Ut15*8>jzC=4}=qr+gp>w@?Jj0~?nJbz8s@5T3dA zZt0}7?zVW8cKzw23K^fJs43r^L1kek-wy!uOu(RC0YIh*U=|Z)>KYXY2l^p!5+OinR3MPaAc(ur6`#wGjtEV;r78<^R|D5hYYL?Lp0aBkqvq`gn0 zpcp3kT9+2d=E9gGxoTV-W_)<~*YVA-*=Y9MqV7eT59qN1?V%?>0&1tAEK6>MmbtF$ zQL#KzxZQ{4?cEwpT3Dbybx(FK=aSugxt%-x&+<#hs_`+l=5XFLd&wNWN|Nw# zkGHdIoH1zrPMzh&GFj2*5DndvxkLW8Pi3}FLNZ(*gqDqPd>C2ry~$gG5|O|ioK?(e zDW5a?-JHcWLLRk{Cjy?k5Ov3g8NJBOTh$*iq5Yk)=^hj&SI&I^rUwALfxItRpy((I z8DzKTV=aNCVX}sQr*ie)lkN`BD%nfkikMPYML|_I371Dx@t{T-s2&{YJ7T z9T{SMZJ24zTF3g3BZjne#Ea5akm!n>9dO4J9UhSS-NmyCJ`**|Is%LdxH;k!kr_wQ zrgEnhK;y?6OU}%7&n$`)@)n$|&piU8{vi!`F+6J4tI)#y3Spegov_vh#&I)D_m5f% zl{T;_MGewst$jLCgtS+G?k8+I0119d>%CKn zwd7X@F{K#w)&#VI-cVyky-}y2xbH0>&uwkEwu3J24^r9q+AE}p&A+bUIICAp1*YGT zlAsCiWjlhXqG#d$DRn2%b}VI;Ri{7g_KGF3M;}!)Sr16(;fH-`FO4uUrX@ZG;|gcrQ?2lnX^Yhgd0(w?7A@aWo<@bRAzhqYMf2uoT32mko7F|rT#uBz zs#Y|EOZ%@WrLQ7we?%SKRK$53zzi`RXUW&PV}3w4+>u0n2UkmAl05f@hRYFAv(Q<6 zYJ~J$pzCF)~4});Tl!0oK`pz-7|2)$XNl@MQ zo-vAQK~XF#mLsOX=MdYSc+{BhHUm>jE@-*+%z65e8}yNZ=EFg^x_QgEY7@L6F?;gy z^9;sSVJ0MblhVgn|Bt$D20Gr)GIXQ;y6f4KyR%dIW+y$F;YI1BN`R+?^Sk*E}=_LEGLj1V* z+8%z1X30mpIt3*;Nyyt)=PU^~qc2Jnb{p#_+AlRakP%H&1hIL97Apn+S_`!QGJ}&C z96mPB4*bI!0%UkAea_6N?CP!dU-_KAoOFbO_j}UzUm&JfO(gQo+TUF%no18Uv?dVEXkT^MM#R5(6h+Z^UKLgdwF-M4h$?8|J~oZxSUj z$!4OJSukiI&360k4b6f&gfZKyKjzWRhLyCD&(;k$`#r8j741TkaRM#cg+}wlN#3Sx`5EK|A z?iedyd#`BSeT&;u#{N*!n7xGU=Yf z(bajB#xj+Pzj5Lbb+FReIkswK>7Z5Q>Xmc{BL?M*SlAtkuV587E?Z*aR!@wJY-&I5 zLpDbkje&^!3auS)x=*t9&(}I@5sk^Tn?qBED^2M&5Y4~WKwP#rqiDOi2Ue=hrkrE( z$nMhFxy=3Ua+@;PoZ8PA|;PO4AaN$?jV?(Vpv$2DrN!`FIdeF@QGQ z+VrCsYWh*En(s3DMX+=)oQ8Gmb;JjdpA!b~co1A+cgPa}}i(5VpjP~(1 zITwt$6d?gTloJYjBu|8q(}!dicF_*-e;O?37sGP)KNXf?$Vi*eQJGQ@N;QBP=0XcH zSSTAeZs>JxqRwRzsLqub_nj-zIUvj|>7MbrmS1oPHNgZUtl%l0uPtB@KqL-a1- z0V?nDKw*x6IPeWf*&&BK!?z8ej0q+Pn^A1#8N|D_06|es@Zg@WXVx1Z%9fhykxpKs zNlcYfeb$@m^MzBrv;qYCz*LWplnF6Sp^HegWE>H|e_kLkenE+`s8Job{4E)@dy8@{ z#^E(OEQ~?Z#}cEhYD7Djg+1X$X=Yaatz&Z_Br<*CD@l6+U_Qmw9J6niligk%#FAR@%mktv9`Ov%v`){a22P zpq^@zME_NFV6PRz-`9?%G@3wfOe8iYSnI8UA8eKfALotq~rDBLyY5IzbAp_<@z}e`HC7`}ufk;3+2G`Lbo2MJQ`G zl1PPhhZ5|-?%+S0Ca>j`n+;*z6C$=S>6d0}b4WsanD$stiTgCGPPmU|*P=BhmnMxz zxCumw*@XM;^x93*n^}!t&1Fj_Ev0%R0Hk}G900m0ff(Gdu|W-z)9d6GZ)+z}IAnoI zwa{w#b9qy9YFVoUjZ#>BC?@pGI80kS`($x*bXlHe3FwN~{w5w6 zGBMZN4?W@cLv2^6F#lY6nisix#LKf;9n;L*XTkNOTdrfTPwT3IbDBlND;=;^`aG0+ z$t&<;DKaf-n6Jf!n#>Q4Kc5)g?E_UNtn_0^;!4+$WT?N(M9e*FIh3(a7Ry!lCQ`vNjW4cYldw`Ht4?TlvH?%dH5j< zazzwU-A#uM62HR~FxNRXR&c?q2*ZR7S1=0*!(()u>`^tJ3=+nPzk{uf+4s$#fcot# zi|kSw$9U-a9`3~N7%1rwk?`^w*R{UUljK3RyScz17MKFQhG;Laz#+h}0e z`%QGu1Os$vqc#J`&#DCgSOl%AxQdZ7DsdpU3_6L+g|w*D5?h||{lx?T$KY8ltO(+l z+!bvJ;#WP33awy`RILXIa?oOS)CU`+F89G9QqKX0?h20scVGyICW|qvS?!YPh*c&& zi8a&{1M6FEIi##`ea46o9w}OeuoR`~)VM&LC)@k9_ZiSByYZ3WM>?L+qy*)Cn7tM^ zd*Md-{kciovi(YQgHJrh>=o*Wzh9fPy+M)&cS+uYyW`Tx9v9SQEO@~=j zmJHqW(P>(=si=j7h)=tkkk9-wLLSO133>205%TrkO2{L0Cn1mNYY2IG-ISmQxJ^M1 zQT~wu zvkHbP5Dc+m1p_(?1|${qaCa&g8l+%ov4Ww+1YvzyM}jVU$gU704NDgG;tW%qmp7Pl zHT+T9CS86z>c}ojXSNz)+>GdumfuYC;xt>`Qfi zHSZI$9SIt7vjmOK_#tE9(xToPXEGEw(;`M;M>DR@UAf=?d@`P7YyE%0eVPo*YH7}; z!!56daHoj-$lq~5$aDB_eIW$d5e|)Zvp*JE?LL=IBF>oyF1%aY_F7=f{upOfM~!M@ zj3x$Rn}sWnbD+Rlq}dn@&ouo~#OO%TwkA!iR!eFo%Mmv+w0lt~$;APaf>)`DrJBT2 zRv=M&Vkt)~v9|SMY1rfolnP=gH%vr1Q^12*@|V37<^*36OO;{*;Kh<9da-0dFP1Fm z#gYZRShAoOOBVEEDL1i1g2*72D#eo8Wnzi#@%1r`+s#h=_V-Tw#I_UmX*UYbD!7oGD!b3!*CpK5}0G%FJM-;bJFv$ba3I)0BMkK z*YKpP-)^kj_#WLOO9m7LHs{fobU;=K<~ze=;=OQ&Tmw%)_cPz_uc)y)m_SJkm5|f4 zF)f1M`NxTS{PRgv3AsSQJNQ5J=L}BM%ETZ##H|?zq|g-HDcemdlY@uFcuGUPD~6`I zu(d>M&86z_k1r2LMP1*y0C(y=V={&(+HV@wcFAcHv^-F_``$5ko*j zW@F$$4D64AeK9Z-1AAj&PYmpif$1376$3kCU`Gs0#lZF$*cJm@V}L%iZW7a~Bh_W> zE)n7fQ!ql+iKpI%BlX*YE2t zD$*s^eT%(dTY^#GUv(HO?3I`Mw~Qn%eZ^?zra!hEky>Q(Dk61gi@j<%*fg=7Uos4~8MSn+H;dU%p&e>b`_&WUqxo95 z{qIL@vX&}i2amQ{LTf?^BPzmW*_w!^lrF$~RjE~D8)|Oqz00gme78-wXWi5OHFH4T zZ;pt%qEtKGdvX0Pb^?@P>ch+3a);n2lX7zH_T9{|15~`vQVaV zjpgdVrZ?Da?tZ(&^4oRcDg28yV)p0HQ|pVd_Cc9mQ4r9`FL6kOF5QC zOd{ti8SDiI^3^!juVbJ@$^ux36|ztyl;!*2itsLq=2~q8g_!++ZDK zaoNu-V8g5swO%Ox@gY+m@O7Z8GG7P!xa0@7zrXLiLJJ+`#f(20BX0M}^W5}F$rwFC zkhjPlVd3E;GT}ZvB(z@v3AkmIqlpsV@o=p++PLU}*8Am@w{M_zT+{&}QT5tRZPj=R zqR@Vo<^stBpWTaN*^Pv`s+HzLA?7qxbdOXB7*f8)@=4V@uNI2x3>e(5nFc-f*dz$P z?>v@sYNu}x1)w6BgMYSGXpCYq;lVct#EQ*=BWhei2{>+V?O>vUm0)N25zL=FZ^Au( zCf77Y?4Nrv@jI=nA|W+2ryF8%_L(ZhiB*c%-Cy>-`^&y}$NJtKtGvUXUV8YGBF9=s zW7_U-!~SJc2IZJSGR^GXOjIWiPBIWBbr(;HTGmUDNAf#$t1XSQE~3?tXvHm9lO0Vz z=7LJJrYthj6%7c$xaEOO9ziIbPvpXT zwBRGdLJms(*z5;0g z&P2F8y>>%OUZtXte3UhI?%gQ#SJLnmYA*K{H^}{CmU;oHZKWdUbuCMt=?a*VsyRK# zV!Zv^b`b_486r~09`+EACT3GuEfX`apod914PB}PyIRA%({8HUpkQBt6*~^((Nzvb zFAsHSF9DcZNz1ZYC#9Vc#KSUK0yIAamngXe_R9S< zu-CG?9I^C;7-^K@#iI#(F{rRqGj5_n4l$~IBOE0 z%p~B%!_7*Qy%g(Y6@UzME8xXlr(StgFQG|XmU3$$(L98}4Sm2X5UgWMnYR5)=Lvny zW7`z`X9d}u`Q13vXr}Ord=P)Bh>4~i#a|d_jB0U*2|%7E^cZqb8$L3v+n3QCo^k27 zscc*@wZ+Z-^Rl49IjjNjVTYdK|L61fl62HGy3{r(xKC+HEBWQu*+qG7HeIjMcDhv9 zPk+f8GW8|gSk)pmZuZ^2MXR&Zsc>|s`RdUpgx6}mLvesF4A?uVqNIa%?gg?0FX%%SH+L0T9g~d(8+`Hfi%c4~pw-A+Lu4#|1je zCFnWPz=#_PH;tl|vX$WqsWG{-j`}%QHvvKl5guOV6-%eE>!uAUZMA!|t7SmlYJV1S zMCAzXng(@(1G8jOf{KH~s!1$nuZFgNIF@aVkPaW_XYC!4ULM6^g}?zEwDlL-g_M`3nUUxSu9;e=w-0248A@}Kd(76| zG#Dyrs?0#nt|6s+j+$kP0Fb~$n_Kae6l74CRP&eiQ0Pc(bZQ>MK>M6@jkeXT3VqZe zlR24Pt8sh?ePYc90BS{gWv4Fp+F_z{EuzW{wI>bnhv%lh^iYxTpgo}?<~~<79|V);*Odr zSYu|-+Z*4QCEuMS-H*QQ%^!O4kFI#nRiFKlt>1MsuRKj)`NzM00ug#j=d6m{edM{- z*yG<+jeX>jYV7ef)!0YAwHkZ;+p4jTTwaYmenlAzNEixS{VWv9By#m9Y0Y*;^7s|> z5YLZX?w^l;TZn&ah_4CpOG5mcLj1WQ{`EBS%P#=97NNO&_&dtrmoF=W4?n*Qe)*fr z;KP@e!7pE21|R;$GWg|h@Iklt?WW-|%(bl5c562IzR(*#c^bb1?jH9Y;LbqCA@@(# z_OGBHxYg?mu|BQ;N^-+0EwE58GxrU;LFmjHLf9_u#BV`o4^P9DYB(IE68Sc9U ztS{?Xte%agE0WJ$PAhr7^D_VZ+_#4KH;4EoA%1a)KNlf~$O!U7Gl?V+7fv806yw-L zI7FcsCn|$f@`hlfoi_we*+Eq(bIgmTAfXPLMBz}RJMPoj+V>|9SGg~8$*h^xKg$t= zALC|!f3l-}9iJ3Cyu?{e&TyJ^4cqC+E>C;ZS8d4KptP-d_Ooy@VVmCXyCc}yA2fd? zeuciz3Q;iCsSaq-bB_?9d{KU3^46Jkuaq+n7rQ7r|qzSeCDCU(-p7WmqP> z74+z?*7Fi`>LTQC(R!4CZ-U4>n}H>D@A@0fkvKD)PsbSaNrK zNl9NF?gePx<0&M*u1Ln0*)-E?wZ2pO2)%7yP`Y}M>TM2jzAtgKIp00zw>kXu7Ve?k zwCT!L>+6&D*Mtj{%FPT=ZvO#C6zt?X=W|!JTHj^UlRIRSqPTZ1Oh0V5m~bTNcfO|m ze~CV6i;jQr(};ZbT0p1GOw!>CknV%{>!);9^QE{!B(m5X|D0`L)C;EWpV*eABl?Pi zza*hK~=n8P*wKytKzh-&3oo!RUVYn zWQ^vupi=TKs5E~%mA)B0OpNaN3I-nR0-1Akb+@mEY(4->`had@<6rwEk2gCAI-Qi(tWL-Oe8<`SkiuhJ^{dR@!$mGkg4Ya%tj-{b@}NZ z|E+Se_gLoi{%BSQR9S$AXy)sqPze=G?2gq6a9%6Mex|BoZu%9DOus;i-`oF&fOz;s zIRwec_)vrGt7PsS+|c;>40k~~UQl)#C72+R@r2!kVzSe^NWAF>n1=J#hYW(9j!fEX z`i6+xZHGmjy-fYf-jUCPcCgdG%>2vVG0kIkJk!6-d6_eo7ycLS%Uw1y9DA}k(rRBi zI&5cIUt}MJ+wpMV7e;YlrI^Ceroc*(tG6<0jk&hPKsN^9Q~zql?jdmd{A0g8+TUzn z1u&8~$%mgZl#$xK?L17V{7N>g2`B^VX~3$ zLm~YvfVB&iiwx4{tNo+E#pMcXk7{YVFMJ0HT=65nGy|gA+dpleHO$<1JzYfHR$Ytn zyo6g;A=xu1&y|E{Rep{ODbA*ThRRu$XGS@z@(j4MD$kfaoAOrHSm9}Ry)Vid;~Dh& zV-Jp6akTE{X6;vQ-eiY+)A1(FG0R8M)EYI6t<&-!c9ddxvCf0(Bp3a4mf?;|#<)TC zgSCxL*Bh)kqijTa3B9fTEdUD)%9S6wZ_aT++t_J_eF9za6JISps`G(SooQ4D1Emg# zlD;G$>-uk&K%qGg7B2EQu;;Vi43ziYax5GsI%wu!;`g?#a|en$Wn{H zI6SWo&+EhUW#Req@O))>eqVUr6rQ()=dIznK0NOXPj1|?dR`NruMN*`8UkBmU|S4q zkAbNe;0tvg3jUxzz_;ssU^)hN$H1N#*c$^gF@Tr1FNLqD4;+Yr*%&w&1FT+ro=0O~ zE(Q+A0M4U6&(Ro|kAY(`a6ASci-8j{a54r?`2e$BxE9Q5o?>I4A_OKEO0+J&KYL<4oiAFzUjqx@hUW@g-qU9RWs8i1bN;Fe|3!jSr53wzMDCX=aTjz!^?m6G{XB zA(cruYk!44OG9?qd)x0~o$WO+gnEYS%hMNP8Rb;+dC9U)=5IFD6a*_sA75-@z*rBa z&1WL<{KR0cse+B^fZNMhJ9EL6XZ(a#O6G~J^b}j^DR$CR-L5CyZBLlUp8vUIW*p*| zWzSr8(kzCiZAADPbpBVk#f&k`@Pik$jnXeyna+~!yca(wZ;1gDuu6GbOxckF`g}hI zC2S=VB)Y#qSp4_O@)pC(GNuA0X}?EXBjmhXRl}s0WiO;?7cA68d-d5D_Q!>sd)IFv z+1gjavcV->tUkb|+FyNIN=0UDTKy}Q#X)0PGpvgfl%>G9wyL3Fv4)`0M8?%ukK?PS zrO^b&UGo%q7CK`I%HqX4)XBv=XT`&K3y8)DQpAHqgZo$dL|P(Uz<{cSb8YhRfqS~2 z63ftUEPfI8Tl=Hi`lH+Xqf^yrP1p0)+_SkSkB&wNc{Ut<5<-uhC81aUhY`9*SogRn z>UkP#$rA~{XRH|qKDziRHV_AS9ri^)RiwnZp+QFyKv*O1xx$KBr>x)_6=sF}|| zNAHvKmp@4}muS|%gUH87I52$93=W_E0W^2IZvY+cj~?ld9_^3LSEGv$AT8tG@*&I* z_?^lU_e8z$4Awe*y>QXLQ!m8c+v@pA^O6{k83D`T6E)}=EVuqc8}#}G4Z>RaL=Acd zE5N=6UGQ(zL$OER&})Fyq`o=&?SpeP#yn<|Cv8HQ#|Ibu&r6ouOeeLGg~ZoN2mc** zFzl=0rPab!?m}(hF~F%ZFJZm4cK9M07Ez7mIr^yCBG|HLmdo8Ay-Y7_X_klVA)$O%U(`+t;u1GE=cIh){ zSg75bbL+w0^~}L>-Kts4&ISU8R@ADqfq?PtEFoZQJxd6Ff8%Ug;Wt6f2ErE~9L1sT zY#?|;sB7MH_CU@C!WwTqsd*VzvA60>kU8B~?XAeNxC9BSlSZ(&LJN&sQ!bxG0@Md2 zfFCH{V_Xi7rQubVMzwzmFC|uPdLqE(`7KiKF#v^CFw}}?cjse zK~IB+QMWY1K23O59<#2wAU*{Nvw(0oFbDcv5S~SNUKF0I!}H?sToazp56{cP^M&EL zHauS(o>zzG_2K!l@O*iAzA`+&FFbDw&s)Ot*6>^(o_B`l=J0$?c)m6~yJ-k)je%`3 zussH*VgQ?f=Z>8*uqy_pV_#j{;w#dwjLf<}PWKVDzV(yDor@sZj1xFl_aXEQwg zxWW52#BKbLzin}|sOHr2R4l5YCSUTB^xOb(MR+b@O%15=!CXU)r=iBvP~$1oP{@|F zqdsK_vY>KCK#k#>r)JZwSWqQmg9SAY7Sw2F(zyYh-w75}$nn%=)qq-M-Sh;q`l*Vn z0kEM}QC1HMXdxfCXf!&!#)hXMU$h;SVnXzUz$g0uctXU+FG7%hL3>GB1W{7e3_U^4 z1kT`!m;Yl4vIqhGEoUB2sI7r>xB%zB8lgM^ULOd6@B4pE0=z+cAV^Q_UH^Y4z(Q@K z?zq_LDK>j?Z9`_Yin$aU<<|;g{!}!F#UXktr;L(s)gctQ%a;CNd}z9b;_W{B+yRTO z{6f6}lyk!}fF|xY{{fSzte?BKupOJUM^Lj~B_HQzyHzjpR{NLn0Oj3?WoVCDy!1WA zc`IWaJ1e1@!#plPyHGl z`{q=-Ub$n*?Hvrju6QJ03NA2PiOw%~Lk|KO_xdHAvZpRZiOwgn( zAGzd2Y++RUrEW^M>26Z!ryq>L>BtHhhXZBp)1{)U?uGWm&h2Ujl629bPx9YP#*U#s7AcYe5ExBFI(gOUV)=fwzql}fNY zM?ctVU!%-&*0z**5&*_G{$xw9WJ_=FrSFbD)bh^WhhyN;7&sUMd@0H2*%t%hYf2og zU!C2`nc+L=*jwGPcZGZ>ERb_<$XkV}=k(t2?qm!+76Zp(fDa@ETw)-6L}{R&t?vo> zrYxX(yzQ8JcD*;e+YtlXV_<6x@LeQd&&e1F-%c8+XZK9VH)8?S<1Nh8bKuv)yL~aR zHwJdc0G~zj_3Vg&@ad$1dJeuXWN>NYoEi|Mhu1{4o!c0V$N*s@cuC071XHq z3A$dLyyZ7VL zzY2P32PG0N(m(Y|8yu*=zvLV6>0_Ias;D0TB7s1I`^bwO@dd6@4wvFS4Q7_r@D>;y5j4(Vuj^~_B* zI069fI)6T*(~u2nnQ$|2C4I!@T+MU7B{}7^_BP!qDmIs;(?IhBR0QjOfFphHQXa*& z6Z2aPek$2Y{K&gn&&}<`#uTyR55?F{VkgSjZeo5o3eFzI-W{Qyr0S-7&U(Gs_W?Rc zfuk|7m%OLm9ih$=n<``biES%mk1D4zrvS_m+cDF6Zf3Y0A-2oTrs&;qVl(fJFz1Qw zDPvpr2Ml&_xWq)+N#6URr?RJr?R#Iu{WP)t?~9P8I4UytYcaNs*r763emY8UQ#K$lwwGEwr#lmq|k#( zp+woRL`*Q3oaot&k5wh+s0yqlADY09x0-a=sf8&MUG;2=ATb_sY{F{8j z4ZC0aBz6@ymrt~JFt_@Pnf*KIH^A$MFl*-D%d&%m5N4NzQ0s>t;5`ui_D-fns618B*AcUzUA#go?6+%}fQ6{zyLg+3DVMRZL6KqU5 z2IfgUt9@~TTPYUr3tfX#X~n!sBHWJ+LYQ9?g09S|K$ufW)SbhF5ayPIQ0Urt_crb* z4gBF|h`;L#y*L-qyFcclfcW0mXP?OJWR5$>QmlrVq;&iy)`)!+fcuG>39hWCX42k{&u3=r7!VUt4Y8f0(q?tbd8!mXFA(!%r6;pb9xzq(o#`b zSjp^9WvJ;Mw{g1^!%#Z<2lin;eJfzdki@@0gCIkCaO#8`GT1-Wr8(nlLNM*)XfSe!9KJT-iSR^~6j{_C`(C2*ilZ#|$sLc*$pY_>KEs~vaH8Yr< z@52iB(~D$h5KRwepZ3|i4=kiR3JJc`{kTv0>{|!3>q9PXd#{%{LCyf=!hkry_%?(4 z(x3F=d>qF85#LUT@A(3i`%0xeUo&xk_W9m>KFi|1{271T^=fXd`&!h?YDd$&i(cmBSntF9;H470VTa6p$B{C;Oh9Cp8Fqk~V8s#9mTFPi(N zGPj34?{n{41U7@?Wo2#;d(P)RxCm@=BNc++_psMa3X%PbfMy`ADKmS}YYgi{i$ErG zQD*j#Id%%<1B*ar%xPxx^N)wj8lKFL4no#i#Q#_7AzEal$U$G86Vh%N%ZjWTQObP2UUOlJ;tPX9UfT4+7eopk!++%^ePDQ7b0tNSRcwC6AU# zQ{V=N(inZMc|N3Te(;*RT|IOxBnM5~Dmq?*D4{-9ChZ7NomqgqevWY>ZP`ac>7cPujWC^0R>n7(dF(C+U~6C4o9zL*z+^djp!eO~m2zWaA@|K`YNKVdd3nfFGZ3 z@?FXo%Y1;ZQ278K(egc2r=fVVue8hCd>Ex^LeU&MBd-`-6S{4~9Ns6n?UgGQ`PLXl zu?XyN44jOCtr0xsUntuc1=6Qd00$C0H|?q?zTw@E$ki1@)_O$HO@xVvdpy>CBnD2! zz_y4@!%kfIGEX7DHTf2iB2ekpeuNOMC1ut}gt#_Z2r=cy>W{|2=@{5PfRKIM{3+x& zgtlY!C^2GI&H1Jvbnupzd5!XWAC5U>EnNtn04K9 zz#aac9Y#~UjBR|_9sd%yxY{zZBkx=D=RK6ZAXx^iSq^z|Cp7m6y&!?zxl-r_2`Iei zfd{tE+P7r*-s%gI7l)iL;>qIFnHO*J694c%y-DF?tNmk*e_T$1|xt6k@BA+fEf?&rV+$BE;quI4i_{4wqBv`y52|z$=^wb8FY`?`8T_nfhe=K0+Mi zq?ctPUa-e8zf8PW@|iCCJMkvt%EN!FJMT9@(>v^IFl!5LcIp3sE-AQta(H#yL!4X5 z++Ob`&mDq%3ZdbxLd?EXyyX)%S@YX9@5#M}USc-(R|m;96HNV_w3qb)dmvW}X-<@N zooUQl_H_`S-fD)_umzx0DCsu_>^5H+>y^qr0F|&^LNkmBMmT>T3iJIE%hpgvy9Qxo z6K{<>=*H$|BEEW^rxif1;uxf>`J_CK*SDG5ZtX#Xl5H2^+tw_3FLE{zt^&<-o zc^(d(BBmfi+Mgj-#2B&+m!@1L9MRuW1vQwH7Rd3vc~9$!AO=KKvno)uuziq6J%ViU zO%}Clm=t&0j-2lVG_ks4i;E_3b+0aIWr=GN!PiAPvcqTK+_k19ZWQI}wGr;83hA() z)y5QAG$%ROCzhj#$ty?o6d4hbL1-}!h@@kTSQiA`8EonpOVwENEwcNZl}=s`WK=o8 zPJ???aF4{tuPp8_!hi$pOL-bB;sAuMh$OM;bMiSa7_g52PK^}W&V+Zx_#-gdKW>dYnXZ68hxHqZQ~nHkv4 zyl(rEWDy>apGW-OvW5@aNc01NvUWds?-)brCI5Z8>9OF_C;P7TiCzA{2I9*OVaq?t zHm>+KwKq?HmTwcZ*i-E*;%|etD>P313Heu1C48hzOe>6e%_^dRS%-zzgAcVJ6R&h- zk~A=~<{KRsGut9TH>ri&)X0R)$%rnA;V*e(%gzFN=VGlQJTnqykuiCw^+BfGGhG6ur*&43=HYJ0v z)>|{0DZ8ejfhoHdmaj5p*ZNJ_y6{$S+kcPk&PGx{6wKMk8#8D3Lz%6baTh?*&E)0! z6+@Aruz+A4;ETdjT%( zekho?aiTQyc0ZKv6XLR>9||V!S}<|b*x3&vQb9d9s z-6Jx0XD-~qfVmqsIDbKd~yJQ6P|1lj+%|3TkoXa7sF=g#>lN9Y$JX;GrPBl zUDhPawN51Ux7ddE6o!{zC4U_U?WJJV~3R0Hm3nL45FE} z2Qn7}0L_|R&BMUrYg~&1G;4O5xn~(%V8r%9YjGnsd-5`34`e}+ER_IBNfUpfbkB%g zlM%bDUDoW_Xfslm3K3TuKa4s{7xysaTGkrobzFQ*=V~B)8uS=3ZHrp1s#6lrC2|Pg#!A@{E_0C4@sK7>~k*QXbcW zV;N29^b_(q!s4Q18XZyfH0w%5#-$pU6`aV78&ZN zUq5Dgm!{{sHFAy`DVkh1 z=2q}Ea9k$U{%+TN8?}bq8nO>pvJc1X!-ajgu*{C11!@JYz);1~SxYwNbbWgUrP~~v zV(omygxaNxLzD&k#wY05ysWnijnB!O~?~Oxcl(^|g)KvRrJ; zg4f@hMiSrmum8~gtzns7l=1ey86*Klsx_30SvL1FL>#6P+ztGZW zbGZ8rO9(}Yc5pwf*^BS97c;!TdhK7l z++N&dFFd0JjOfYL9dr45^vrqgxq^6z01QtftiLkMj~;tWiq{4k2*r-;TPE6m=*reS zZ&`GO2_f?>p*TX3_gnS;Soxk%|NCuvf4qE8sQ>+Ty??BHPpJR>l-{2x-xKP8zeDd& zmhTDmzu&3%r^@$)`rq%;`_tunLcRAeIxU=5j;{5*C_u<^*rZ^4p>vS*;~K$6Tp4Yl z>M@Czga@B?@|nD7IO{^6Sv z9;IWkcOFZ*4@RR&h$fv8NdfgRiZU0*oHpJySCg~s_`C(r0HyVyCL{PSHLvTc`4G4}@5!4Ihg|kmTNMxk;)pwj<@sRS;<2n?b z`y@uLDe9}dr!1crQn@%VaCp;ahd}3;_KPbWI}YNDQ@88{$m|JQm{c6>{Uu2od!Mhh z+pUw4WCoa3llt$h01pMWqIJerp0N(vM*J4fp4*Khu%>Sj<9-|sKm;chiqdZ)ZZX1+ zuozB;YEY^W2T8bqrOaANsuzi&$FgdVNmjD62-i@L-(iG}i;OPhjl9KGzo!n~kk&EJ zOCCip@ZYB6W=6agI57iGt0`f~ndudnnz04Vii-<1W>88!;>K@W?&$gLEU;JIOb!5d z`5lFA0T>#p)}14JFjW@`G*rSqB)Om0^))8Tv!A4sj|)3?dvV~iImsPB@L+Das>n1& z#ZkpDTR+aoS)JWFp1IlXO79esvBSuZ|DqFozAVcU$!rE2gkZFxXIa5J<(em@f>(TA z2RfntE7an>&k>jtguBNB*+F1PP9 z!}?in^|9OcMxC~*3H3dnRyzN8P${J*@3D+x$GsMks@WDU?$i+!Nw?q1wqL>6e(grL z&59E{X|=g~H&_KFRLU+S12#BV+3u%R&1p;E3SuBiePn-wbTldtpB9yM zbSP?H)dpc{y80wMJD3N2i_v;Dr+{`q#Le1^_A5MCT0m1Um9@Xub_1b_Zz5CDPRrRD z))r6hc7>Qe!=gNPd_WZFD{piXt!PhdUFNAm%N#$w@Xh19Vb&ao2aGP;`flr|hq0-z zLP>mu8cjalOtTB_w);0F$vyf#m=I00(&VV>`%$1VdO0#kNV=flgpsgd`y({a?e>A@ zCUFl8;dQ}&!1Tld4A^_N>ftZbx1puy3zTfW6E&hTSVuhc+&(wuwuKl5QO{zaV+#h` zy9sQ-p1M9W1$^jGDGt4=#l<=~qsp-0P=axcHBP)%a0tX23R=5AZmNA#4J0f}rY}`A zN!k8kmIG?;))3GlblZo05qIpAR$oS&uBENUe%GEE+W9-)49j|5sgTLbgT*0hTz`kY)RJZpK2rFbwoAxLo{r|a zdP9kcWE4*%%I34KE|NFho{zH?FJ}lTSR6=Ul`;D!C z=TINd)T-B|ygg{AZ-~eXkiBF^1WAP{dWYWC(x;%<24)u2SD%=ZL-zB_{4VucO=&|> zoyXp7CAIg+z>XCP0B^utz+#0S05lKP7+S|ewlWuIO5W|!s`wkKs;x7=D!)fqt%^5K zrK)WqTcs+Ji~{js4%AguyWi)l@>_t_s(ABMs@fB>RjMM%SLJ6_ItedKV1g`rJ+8?CgYW3!(3wN8%^^vK60F7DNN2_pxNxNYZSoM z8dI_1tkG;!o{85pD9Z#tAW#MOdxF-~xj6_*&LCRiw>n@+Ndu8`v*>}_1RcYE5MOEL z*c%3l6@F!uqUhA!&u(#lh2qMmPF=c*&j^#TK?>_M1%7tR^;$=RzkO%T1BIvyfv$W; zK!;0A>6nE;FL*{kCEh)zbnZf+#nT50sq2tnNPvyFJz9>}MI5~D%#5Q9-eTz*?l?sY?yX9o&rqypuPAegYXma1e*w*m1k^u$0rXvb>R!~^) zQGj9*1)Ql>&Xfd$;_#}@b0j-)`s+F=+Ddv}LAoRiXfCoOX82DJ< z;e`!7janG)Y6u-NSy4l!uVZatx6`DlktcB%s?W=58v@B!-uz&9!3d^dlHaCqEoSxB z=6=2PdQW^UUsz4+(fM1F9p7)5WW1E@QIgX`2^?bPmqAJ1hyrb-C&}^T6`bL)6@#DJ z{&LO;V$Z8=c=4+3HP)o-b^NQn0s6_wSE z_IPu+)KHzA%lS!QvP(0hb!~-3G36oCuk8%kMB3Z5(|H1Zp%jbSK31~%x^Z=R|oze!^)*SH14dRu|A?f zRdxXbyVxxe%cD_uNOb)I8bUM*eP#f7L5}L+zzpEkVpd-fFJ=J0=VHJX0s=7=<>(N? zncWMk%r9RtA;CR~`wl5e3kIjwaNws5`P0y(DDcFZl!)lH?PNgPHV^kYQ)ziB38bR7 zEd@+^7w4QH(-Z4mQE#)!$*WVk`$FVp4cLJkHbn)MOH0J3txP$NP>HT>_Z-u>I9es8 ztO(I4^1$}#O~C`xsRqj;!#m8l%!!a^K?D1nr^a>aS2Kf2gA|QRu_Ej|J$qBxz_hd! z0)FON5&@gZO7Ef-h5(lo)16O@05igXbYBVqKLIR>fK3P`0xJ!HnqWT(0!xY0MTp#H z>Z05A6KCZtwuP+D*6|BTTG2jd14EC%)m{x^ts32~#4erM)kqZakF7R`h*Y_!uYdkC zj7bpcjY-ZuFebHP;-@A{%s>UHmk4|YqRc#6T89EZCtNxG*y0o<*zMURYoQJ(SK(nm{#G2QNc3h}V0!Z@0dL$S;7yN4fjKj5xo2zF z>d`kf!ePu^$XIdC41tpmnwh!h#WET{muDBbxqZ9L)xz4ooBZZbaxg-eje!F(us;UA ziw~QZt0FBl{U^Nbt(Kwsz@+W> zcQSSPUJCQ$b%gICMwsDqzOwtExyQ&1sJuppWSo)!^*Nvj)*hkLBTBoIaL)tgC{0Wr z{{P3`+d%1ARe8S8$6KFIRpkK!rjsV&nzN%t@1zYx!mo)O*x&o_pf zQJF2!JAI=J{_Bp(6`luT6|C z9T+*(f5XG<@>6NiGAy;uM9a2P&%K|dck^DQw~ro|4@(li$gxd-I8kqaogjJl_oQFu zP`>X>c%|9ijftS)MN;zF*fdOQn}iXKr>2K#b|NKq=%h^}5XkayY+(KvsVx!3p?2n* zhU~IeB*;ic3_Chu>K1(@l;rt1w4he5i&`q;H)}P)LA9}z8n9Qu9H>T0HH*$b-s++H zHh?H|8pa{498i%x!eaKZ-4xyCCWox5Dr&RArgvDN^D`W-Q)azcqR9oOh8hmmR-!=) zyh>`_C_li_|C;dYbTxJ~Vi}I&HdljTIvnxkZCx@BN5h2RrwrEqciJDl0rk#RkV`Z> z_L8E}!n&`pTfoKpHl*oc>%(pLgBwl3TL~DWXl=q<-_)zid$cp*+SPO)f;QKNSwYL! zhIc)Y03_9aA>mqecrh4DFP~sklSYz{b1(1RxuVQKR)&>@m3R+c#J-%C@5!kzz6?IS z2}=q(&iSO?uc=Qo6B_x|{S+?W0AB*^^`XB{0MZ`_0i!--u=3Yg!RoSb9j~|Elgl#& zLV)dX$fHNG(0*QQvKX>vf8WwvpKay`!|*v+=JC5Zmx-R;in;xJy2+zUFyz{RS5y8p zx|~7pPE6$ANF;x~k(-F*uQzftk^J>U7U81@6L1iAJZV37-CuklWPt;t;Z{p}Y&6_v zfp?CDtqRat`pJe;ZLALU&^l55r@PlwaWkrdNJyns#l?KOxY5qHJLY4EFX^pLH#XYY z_QY%u;G281&2(qm8?!-~=k{jPWsP=i`(idk?(yDiT-Rvl+8=YVqlWgod&)hHcCG_4 z7lLcOHy1ZG+PMzKT!>y}Z!V^8J6G#{jx<>U_2lYg*$}f#rE+obcVFThUoqjY2;E|# zPmhMXY_#rK7Vfsd55I}fVxbR?hA9jD`EP~ImizWEV{>Go$EGmKu+Z+;giRKB`{Cj4 zUkpTHu*wVafs@ds*0Kfrk$d+b!^o!@>>=eE4%PQw!a4AD7=P^ynAyd2OMe ze-$%p3*Ak=ZTE{B50W}#ao2BW!LZQBNZo0v`$)a_fWY`T^-WpmDQekfp>F~9E(`4g z5)1qQNG#AgBJ8os_8k`XdYja+-$J+0(mo4qA?_i||87DXeyNtOIyJN`@JS3fZnV(B zAA@TPeSHtOw$P`31YKC@-p5$SEcEF|VIda!>TWi47W(!h%uEY??&V>-_3Gy@4?8Sy z|I5R)wSM*QhMg98_~qe%LH6$74F@go-c!O|R{XkC!rd0wI2!J?z)wcPlm+fRE^M>F zXGX$y3w#2^?Xb{|E5ft|J~R?`THsfV`z{MT@T%~T1wQ|(Fk^xHKj|D#U zvar_zA2=fHv%vL-hy51#^OfO%1r9=j2QBpLSA>@JWO_7gu)v?aG;FlMRn)!7LQj4T zmSLfS5PP%!Zh#`XrUBpfmRut6_0zlZ8+c07>>v-Jbi4(|(AZ+vM1ZIb_Y(kNDa=s;|~y zm@%L}OP3G6dkA2DM5y&1g&sIsMpp!WvN-Iqz{gGqt#4|AOf3!@EYGJ{@&8oweM>3d z6$-52ilEp=7I`uk@8HQJVgE{^B2V_X>34ZP=gB?!B2V_XoiU&B`{ASF5}5Pxtu3^)ZUoO zcydoJ=gA(mFXl3y+?`9*;NSaW7S=*rZKtt9XSL17!`4@ulqb>RZWiWQqJ-b;LMmn% z3XgwP%Orh8VjwTey~+K=vD6}hX=NwUZVytGi!nsg@2gX*3Axsu2oAScRdjW8o{fjU z>cDXCv%*k@r>!8p-)u6ZHC>|(#0zU0KHSwb?Xpg32g5)zfodi=%VLd#`u4oiW;X2H z!GuEG6(VLMAt`wcjeU(~o$k?W`;ulE(4UU+6*3!Jg%sI29}kG3rnuH!LIl3g;84$!CSH2H(2-^=b-Lo*Jn!;u_ z9~y1#;BPouQr7lIWzI^5XSB(nUZ(%_qr7#0?I(l+`ir65qw-5+H+uL@R4uGiU||a}ot?)3!fAxEDJ01HY&Gg426+HyM>0}LC#vof#VcI7 zQq2LDes9j5fO4r9Od1Zy(?5k$ZN>{o2u>K zrj{gq3EL+7EAI@>Z-Avg%$RgdM}M3x2C>?5vAJS9U{Za9r$Om9+e|q1fj+yf7cJ$m z1FuOJ$2i>OW|awp%08ytgFeV&E^%R>?Iq;FFp?Ms%Ud88woIH~$=KhFI<{JNbh%!x zktnl>ld(oKYqYj>&FWa(5kUqoHWB`a^Q9s@_ycGIc%q@bAglgYuZ<1K>aEt-KQM@! zCMZH0jv0Q?aqQ!VqqEizQ>iy5`9Z<4l^@Ja_VEJ&Z+^tC-%LJ|GKELjxq#tg*!Xcw zT^_?=fUIlo2d&(*(e_W`1I{BzL?4svphX&%?QlnH9QZk%OOtWN*a2y33; z36obz_;#c;Gq-T4x-A!5n(XKiL?nNLJ4?eYyg^e>Gm7p&LpS`?Q#3kioKZl>0V(+$ z6?p36^*0;Y#MZU53;%Vk@VWtK6+g*>U5P~tA@?Fy;2pE#twkzQ9;=kH3TxXLZdz%a zi}A$Iz4s9t*~!P!ijhee(8iE%Z{*6oI10wDw?38v^j@Bjv~Ptu$7YJAVe4ZW3zkE> z2J`73Y6l0}DJfTwQaG3d9mU&KSDlzDY)wf6Hjhx=vey^|rbXRmwWzgA%^M zbJmgZ@{|IQ&pgtGyxP(N`Tj?f^8F6^-bV!ale^my2Q1?HPQ-@Y%5l>p?HrpeVsBAZ zaf^Nb%TznVtrqdgS5+DAvhOXR%NwP-#At5ds$9q|dqb~at16U>s7p`tI&qoH(# zb$B9LaAlKp&C=Q^#qjJkfjg9=S(?N`Ix3|Qji=J|b>+6Ojv~ajCVhl>faX!Kyi>df zbjkm6xaYh2ns!da)r)Tty-iV|PacVjOPsZ;OQLz_-2w!O29ECIQgJz< zSB!!if1IR%f79=gM+*`c$@G#1(+b^WoYdiMYulfde&}61Jg?cU%ZOk6bFC9tHCBl* zRMrDjaOgh(<>Oyuj5Q6In=}9~Nu!!a1E1Q3G(?8pPQb{tz8+U)k>Nhe%~4!5OzOF* zdf0@r7G*+#uR~PTTfZa+d9HF`hM591=D=8#sUOEt*FL1)z41Yj8*qHr1Bm`C zR@c9boO8I8?u|gffXlt?COltMZG`&eC+Lb8)dwFWV2h?tjj>vq)byaukG~RgeD?DS zbc%lKL3ZVjvj$QVcUzT`OVWU;X}*Y=hr1s$QCCM`KxYbXQPkkShh7YKTOY9ubeM|) zJCb6tOHIC8@jXQ|h3Z9?J06yd=q)h>j-jje5kNa!$%&e%jHI?8=u<%&_z3^>6{49G zYTI0K-Y7}WFeTZJ)-?~YDaclbncWEJ&o!IdzeJ-Fxz>kY9bLZB_>vRMG7NLURUZKD zV}m?^pgtf!u3&v-c78oQZQtj=`sX7j>e}~dPk;LHFZ}q1_rAZBAB*&~tG;;S-~ZXq ze)8DGcvTHsq8phw$L$&3f$*b*2Z>A=$3nsdOJh4~f_6x5GDmzfS$-4eLdY1{P2cMB zdk$dCM%4`=z9#kN%?%J`C9jTH2tY=}<+G0-8mu%WvW1 zMTa^Itt1$yNA|FX;u=W%;Y98@1G??a4B-@GDXXmxEnNds;q&RaveEN20(?GeUXGWt zt{f-D4c&9s^%{1|&tBJ*NZJ<2syDfHCDKIIBl-ikGk+%jv%-It`%m3}7zSnG9c2A* z0u5=MlAa5kJchZ>`br1$btfl(X#MNE{N&^hEHp|8RUUmllF)fpV)5nb`dQlZDC2)1 zQoBj={PyLSo}Bz|eD`3!M%{~)+aW#t(NL5SX`3jb?HU{XE3jyKFzg02xc0b#=ZfBo~ z+i$FTz~PTjz5CwLj?W=mNV0th+85Td4@kntd^aE2UT<{f*MnjI zuT`l)*u~f0Uk}PHcMfUswY&XA0Tq(7qI5!yz4OUI{V8H_H4(O>6m)&sL(Y)OUpspd ze)eTa8mMW}=J97z1B)7)jZvn;sCeaqup>U);;t~c=~~IH4LOw8@&Y&EqEZfj9F<=Y zb`X+hTv#6SLA4YF7P>@)#F{#0D@lR(iEo?BygSg4=zW)_J<7Bo@LfKhm3y;B2}S+w z<0kEx*9}b!+szW`MdVzJNM*7jMdPqAwQr=J)ECf@uz0J!I*=>su6-OgnNzXe_`44g zzx5FDw;dw>-b2K1J4E~w2zQU8|C>gz)33u;{W_j&cG+_{yvFW}9*-nf)JM?F4c8Z( z(Uh{7FSOiCnukk^(^o&Nj{0*+vt*3IHAAM6L|rxjQjSj+GDfVMus7{Nu)^lf{< zs?l`tP==RoLXgJsiPXxrEktPJx(Rg45|1T<>ZqBUmHMc`5?b~8XvtOmSELJdSuYnA zVZwf&mR-dT%0m2bQ6f59O84D|dCS9u@FGbirUu9@RzGID{NwKt!%TWB-&ux}Fa? z$)z>o#+ZjFfd_3#Vqh?hwRUVfJP@EpH7a>VFhGRepRq5wWge-z?w<=7vH|NnW3+U& z_MgajOD(P`1N%IHd2Kxvh47_89k9^^8`yT^ez$xgYZk||YWB%j9!?@q4^w5c#@w&1 zS|Z00R3d_4ZPsjhAml!vV?1!{8E*qebQNn?7F8Wh(U8mbqW)aoOv>x^-IXhQS> z@nBc11Tw0%Vc(WSav4+Pn1O4k&H&iJ0RW@OY+Xf)QTU8t07CS22u`Gl`~q4qdwYXp zBdi45fZ6U~OLm#H9r0%qF zg8|itJM^dzh=_@dovayYL)+j?ngk%z7L?gRcRMs=U4~*@%z~{HCFZ@xpZ^QeZ)5XSg+oAznK)VhS*n$O@q-$RH|BPN^yZcO*Qoqr~O#e7yDw-9jYN?*6~g$V&I=}X!3@h~;OhNOSpM5tZf(8hIW z9n2qOlY|~X3yF=#!gb{zvN-Ah7`hLWpB`Hs0ED{2G4QsL%W$?Mt^`?;MO;h2$Gm<>sQiH|TtuR5DFM4uTp^j_MKS#GT3PfOUQ_Fe{Dy*o&%I+v?k~BOO_w%p zGy%)*R~t!g-Z-+_L}t_O)y>FS3DA|27^|(6$0YPllL_;1qsI=djxl6r_G{;BL-~SJ_r+A$EwiTb{5Rxe2 zB9+>XOsFr)JLYD(BjfqE$|)`?QXOP6;hgLV(HPdVoCbgvansDKg^>YGe&X<4*w_VC zm#4>CghZqY+g}u(!CjE}J?P&o zlLhd<^?m&fNTQxn2(b&sK?NePt+&63Mo2cD?q#%VjA|vD31A9P)D9*i zJ8I+s{z#bz46`g#*k%-Ko*~&ayBl3u;%QaYCF6O5#y0 z0VxV}4$(Pa@HT54hKmG_P5RiI;_!t~E!>w(;PDqaiCSJMap75gT&O_WMzSftEDClV z8-hTSQvFRX3Nk{VmEx*rfEr6_IQ?Nb!8#Kg-0(?XwSiJMHQSR$ zstz*Nn#DwF)Hy6tr>w&|6%am1r8K#se;JlV@1@E>g22VV*TU!T=d(QB$Wd|ND0xL% zlawRfAOvP-Z=KSuG<=qeS@1v^vnX?aPv&)9EnVVSbr$DY<(jrr?*(01-|Sgu+F6Ht zvYywK^<2-YQ#!A8q$lfZyRwdZRy(c}sAD}jPw2|I!gF$7Pfw^7`=TMngD|-&6Hj}lL zzQIU=#>;TmH|VKOM>qi8^E8c#pl}x&^Vrh2Q+bqucl^T4(>Q$e%15d`nfXmN9^)Z- z*&#s618ZS1NkhRwwFwiJE914MWY2Q=3*d}!C}*weBfYfuJDc^L&-$*-`tF$OfDEU8OU?=)ppI&R-EZVp{^p^ zWoo1-dj)M}_FAxr){MblnXMv>D$T;@6GmZa;WrURduHKF2>VgDo2|EE{5iyP55n@$ zBi)a%@OjE_50I_Q))1cmFy#VLPK9@#mY$fMDiteb;YCb=mD#cNRBY(AIy6C+B(zzQE1H z=$TSX(M!)1K3BQunZoBOOwSZvqcGPk6}CgAlvQWjCYxoBP^2curtZiZ>RC}7?>ufP zPhTo~1>7Kiohrl40=>^HjMFdi4-4biOISIRJatwF)YANbon`#cyXE{a2agR8e+{~U zH0&5ZR&8fik_&X`-1Y&eS^c5!DmSy*tSf!E0L47tJG78_HcsV;eAq#SrE51>=0CEI}MOqYXDl33rj`LVBX zvE>2I)~T`xcmB$*hOi7m>BxQ}^hQIR!H_`9s2#&lxZ&#p!k&nys@6T|dMu@SH#*Y{ z6?vIN`5MhoGMGKXd#@0HMGpna~E-jgDjC(<5@IL70o)$Gg0_Du^zup<%9qCM&}YOndQ09w$^oX z<_?v~{L2czxebRGmas|TENqys9K^!XrAD=ybvG)>)5>j}r<@81W-rkCC(#)4r{&*t zi2N(s`QKsrkwPv1=0oJKxAWg(2!+dA{u>XGzij7!hvkRQTmG94kzX)7{O?wN&wR_= z%-l8Ru&3gpc8U`wH!ceA?>ycfw(#Bkn7cTr*!}^=y2XQ;@KqGgOtEadeOd@R4o^jU zR?Y))+v$g&T)2D*#DQcOP^xeg``2b7hco&^pMy3MUnx@BoT`pHr?qO!Zi4L^OM!t< z**Dm97gbJaYD+ADEf9ujtIM86aK~!KrF_gLP7Js^FZH>^H#=ne5In z4w>wN*&&l%06%21%j$v;Y!8|2F2)X-?2f!h=ITJ?4CQY{ zdpDVewPZlZbiavfYC4Y$o2$UaD$uF|2d0bDTGz0vc%?G#>-n1GUXabOUq8aA*JaOe zb7V9bG!_{_#@d4-s%FZyKvtQ5Yrat0Ob67~BrUYE3Kl)29R$lpMYH2!2gcV;?XNnd z28BQVSa)Tb%|Vsf21tFnmI7O?xGGaGrmerK%rcd#Cl#BxBusxbWh$61MzuiUg*I?R zO&4ZUfK5>V9Tq-6JcY2sWgV6}?0{u^A2w7Mruf_x!|chzjWN86FfCK@%IqA%z+~YS zgn`z=ClCfP7CwnED7Nsa;XC)MJyt$7ciPm|bmq5C%&UPk0|F`1*O(wsN zn6|vHzZwN?88iymn7CfddYyy^)eXvO?kfBKX*I~5__t6AJ=e!n_7?W@FzB<~{@F2B zhOLGI+Z4fv_vdk;fC+D5Y@G>jcP0#oNHxon@{z3kTvndVc3~H6nO*8}iCx(5<2g}l zjN`$J>gnntym(F6GjNgiufSl}=dn~FNWbJV;zpZ640!=<$GsYlwcG3Ki;z<(P>Ta; zI(g6|K7^>Rg+^stU0G<~I_4M%BjDK|sj4Y>tT50MqXTDqob<81>UrtK%H z??O_%D@_fzlwIEDwd*zxIcn_9ZD9P-WOJ(>nc_j~aaNpa8G&lpa59TM6s? z>?Ps~1l}-;UPXspF=5q`LL+7V^|XXFm_ycydcQh>|EHqH%qt%0PZz9@(4r-`Zipc| zY}l7(u7G4t%FkDd)^A;Nw3baiO>{(NJc5z zdh;-mo!(9vb&L+ma711jGbzU|U0Ry+ScTA8QMjn;W_nnFdpn7vv?B1V~{ykOUW zk^Ag~%tLwc1kuW6XD&+58IM}bL-VXc&6J$A33y5!u6Qw;AEq9~GN{vz=)&3t6ER$3 zoP@(au>*r#tB_FwlWb>SdZgWNxTsO1q!>_08-tpj8D45WzuzI{Ro?%lw=ks zOdKY^CUQT(z%FbK=VvXM2v@?oh_^erBjHLv1`xuBb8+L`s^M5C+LOV?K(?$|UXfy0 zh60|dM%z(lPNXP~{9~?uGbF+`5Y7^{`@@>X7`FM0)b3-Rw$zdp(ZO^zZ3`~~>M&WV zc9S{&mo#({jWSfJb>V{hn__$m9VJK>b-E3^VdHQMC#YSLhrd+C=TRZ zQBxHon~m*xF%a1pM8X3@b#owLgv}YX@i*Hc`*ac`^Yu<4XJ^LTitCO@M<9HY-2F$# z-jc5_uH>KrPDJ0%51k5dkn8uIL(Ak3bz9D6VoPnPKX8CN&8WJDzT&StcjPh*){f-?UCm4W}_L z=77puwLd!<(`H&XhG0E3P~gY2Yq9q5>C=nk3LW^ZGIVgg9Ctr+^5CUe=!m%{?8%O> z(Fc?@R_w7m14sUiKK0%1Va`_=qQaX{Z#xji_Sqyz9G-n*zBa9fO-=|xy2Q5bG0|8_ zn;|DfbLz)kP>hQdO8qztI%NHN9IdBz8*womu6O&RTbylMKwAdx>iDBlp3FDGeS9I7 z5lN>`PFT2r4Yx^iV8cby95`@{)C;d1gi;f7EutC*Pkr83oG;C(Z)%>t;u5L1jqds~ z)fYC$IcJ>|um-Siy~MR|s-`h>5{F5>%~G8-L!G@DJVP&daacXX0-uJ1_FlJG$*^oG zD1HT(7}W%cLw>%^Q$jLaI~>$u6gG5;-%PYAao|Vp;)CU$92^#NvgMhYc*q1bgYZNi z2#dR-raS~Rum!ZshK*4Zo<~lnDXhD<#=0`zNnL9vd0FV22&rx=7yOMHZiZ>cw$XN# z5s|*mYYVb#0`yzVok{2WBrWawWVW*Ga{zV;f=}f6K4_2xP2WY+Y)J@S8fwD2kcQ8x zUSxvoLF>DmP|ZmyJqfQ=bCMQ$5^k#IB-K3$zg2URmU|M8tmY)G@FYB2^(KL#a1!Gu z+BTYu-Jvs)isa-gmWi{s$uaO;W&zF2&PcZTIEP$#Q6s1hlSYcNRx{Tps9G!n?WqP$ zr>jKh@PQ3~%v#qWG#_MOL6(jk69D_vE%e3J)M`%!tvsa1B?o=AX;dMgBk}0bo-1m{ zS1L9y%>7#{z;MGbyIYz=^R2Wt4Z;>H$=k1;J`m-7xUIVne02AL7S-pueITFuK>8v2 zK!&&t@AP47Y$@F3ft2aV3QD)rb-v~~7ec1Axui9S_Qm#zP!I2pwPLeX)la3?kmDHb zhxs~Wf@E48SrMs#jX0E)ty*H1?>1#pB9N@~XNkEbrr}-#O~`E58?Hi6!wNQ4B@u{C zhHW2_+DVJp)y_AiK!=X#RT6f5q+n4t8JPhV3kj{at!?TihSCI*ZV(m4XJ4aj{yxm< z9^9K?pa}iI&8w?Q2)mU|1wRI+3fWKA@8Mc**H4o04DRHntdYE?86RM~i<8Ag-6?~`b$`ETkd>53ov+epO+Z;yeUf2&GK zx`lQOr~9oJ#N2yFjJa`Z%yZWrF|eglvQ1WjTdF{3QL`mY%1)lAF)J=aVYeNZWf$WH zm&YQ@EJmR(3`YkSn~^G;S^VeLgRaKZky{NcucE;Oa&JnIzjg*~<0$0#ioj$Z?U+FO zt7+}3yk;K!C?<+vYbmbbdwkrO{0!v}6LQt1~3pehDw~mt_%uO*1rL!eBe_krP zkbgOmkR7>dt+cdymG;C+LUw#B336BlV!zAJ*Z6%IzhB_@r7D7A{N&{2grp)rIa#i} zFI|msN@FbSy?!K)`O`&xjQdi}gv)ZF&rdbABEW^x@~0TnJ&lXur$xz4?RGWy4pK%t z=xD&-dd5Dp__;x!$EP*7huY|0{ia|KoAf1Z8ip>YU%Oi~?n$e5CuHU+LB+OmP*7!; zd+FW8N+yT}1tgcSbm<@olmJAdvOqqx;D* zd2#V(A5@E;X2wvY3;F_P@mt(HY&WqyVS*#u*bS*`lviL7-IOSl2gpG^(#jw}vnq$3 zMNo$T%>fiuV+TGu{}}c%3r|T$Jap74>6mI+M7L;ImU(#{HAhLA z#aeg-u2c}wDVf9SolU*7Y#pt$=y$ENM{~%bZcP)LN%bB3YLR4BU$N%by)SST?|rqq z8Z%f|sX<-Utsd5~PFL-t>Z)W`x_YN|RUdR!AHyciF7yxXC^Gd`TRl5ISt?vZl_-qD zLiD>7I$C{AG@#ZUGWPTlcf{65i7Fb z7U)tJ0Jxh)olTz3(a(UX2M0YCu4E5}HEWoeVbz~WC01zrFd`;=tk?vigGZ3TBA^j& zwZKIq(5RKw35qbdmIECdAzcx*Ceh_v%c}eAX2G*w%hWuo=3)s6G9$E{HDJ_5yyieB zXI(iNT6BT@6Z0=6666f)Cg@QLAMbZX7pWdIkj8St>4tln&>0)>5(4B1a)s5$WsP=* zzMk|G=*IX28vfBRwz@G2gN7Px;DNM@8g>~a$UwH6&pbrzEIyN0e1;NK!LUe759HJr zY(!L%jR#MbMpT+lJEAaOA2BDKh9@<(i}QmF*)BsQ&ZSOC*#L@PLGdW`6@qZM(WMZC zGv*;99E;;_`B5G>a!M+X3gqL4?a=^lSKm@r3FB=rQts`#s?pxS##s9h&CbUbE5&AB zF^N5fHZ-<2ftE%lv6t8^oKP_dvFA1`poAexC$Qur{M@Xdx*|5{9K)}M>4vg++$re_ z4{_g;8)MQJ6YckBv8UI^h^27vKp$iFvD7~npOU`PL#0L6{o`=^z;0g!7TU*h|2WD% zj-`7`#awFP1hJaPW4v!&RHeg~ZH6M<>=0#*yrl`%qP`&(6Pq@Vu}HECH+{}yn1-`? zjAsy034M{daFgF>Zl7uuLJNkJsn=#G8h~nkGyY63V<%!J928a99q|vx+#nyuco1xy zBQ2x~pM$99hq#;#uc0O`>q;MfJVF5TQe3B`D8|T~X)dbUW6|}YTlSJn)KXiq$~9KcQ(aJ`*j3fVcAudB~dW zB8)k>P!aJB&h9GT(Un zj5{}vJ~bfmiyP5naLwL8E#jRIh|`JiqRSwcgamHMYKw;+Q?C1zxY`oc`b3?1vWoPj zfz3o!*`W3vsx0ZM3<_WhSY^woj3uzEGA5KNW0I(`u|{u6O%jV{(Y$q=z|l;_iuehhL32%tJ5WuC5O(V-ihblAK|rAr?ON3U(< z(1kDM!))@v>y>MLoC&f~D%Vna@KU@s(HTt>Lc+csK{D4|JU|lWsLsWMjG9wm>Z;7D zQbA3S!{m9H#X_sdu)V?~^|gv^C5+%5_4c=dRT?Ys#9M9%a=EV2P>Z~tk`^dD*X zEwt2uPOl4}y=rRFTD=GaQ)1sC*~&t;$sk53g;XVJK|tDUSTbVP7^H-ngUzG`7YqBR*2iC7m{ zhM-i*ylcbony|UWVeotvx%DQk^p*c2$A^^>thmu}G*p_q-&9D^|AMQDn(0_ymZoV! zfm%bY3o|sx9X_}WD?~D=Wtt(NAc)02r|Ze2L>q?$4L3MMD`~_BZZAa<*m)d`?&{_; zq;-gRY+>fjuU$A0u`aL=YCtv$umyp6OCwU_$6Uu<_bJ`yIX%21Wf6(Dp+u`~7>-N+ zFl6dK{dPlCw1Zvg@WIBAFrS9Qb$_~3k`%@d9%RKeYOMb2^s9|2qr!DUCAY3%n8*Yb zmp@N6KsLU3p{>Sh6T#)#n_);aJyc%npBF>Sm`&Q&?Lz3-T7q<>Ipa^0$LuDAUeap# zW)D)Y@z<3`8}fH^+(qHH5qRr9;5KeHUiC{?W*2IP>OxXI*Zg6pA15_}JCv&wkkh`K z)%E-|YEYkqJOid;r&~#dKs%Y125e+C_oa@M2(as3?XZGzv4f!?M8oE?!+LZ(|XXwW3rD8`v>x$B`eO;zCZh-_W^~ ze}n3#UOR1y;n~MRYx@XEx;2e4KBstaQA1ej9kx6ypyCHoo?#>WVh`hluZkzYzHUr@ z%a>L#WP$+=jorzoMYT?&5~~$7y$0s;@}79D0SGh(oApJ$&!w2l{a? zbTVFhjaoOh>Z-RUN7QJn$`3nA|LiJM)RV)y%F4bA`Y$KwnC595r#6my$La0QGEQp3 zc7%{TNK;j=`O-v#?z&d~a1bvL&;$B%mo93-2rX_Xz(T{8rhHr&lOia^KH>1N!kFTH zVamMO-u0=JVa8>GOKY`CY|d0Yno{Gl_i2T1U5odM)bDjD5n>leIy|9zpR^`*h1PeN zHsOG6fTj<2!jpdjz}O#aW7M)EBBrFaoCybOx7pPR9gDaVnIO0jp)r>+cL=?zx^9`; zI>=Mbh|!gPjqRO$*GOc@DwSJ}$l(|{SB^3`4&tN5*hAH%Vbtr_sV=}N3x6V3_ktil zJA6rCuW^lC8zwE3P>Y<(@ba`fbATG-zJcdn(WWyqQM9Y`(Th;*F8J~8N}OT*TnQVu zn-w?`QOp&|w2fLw(QS;yAPy;@aqU(TA`-FSFn1Eoevn99U9K`vC|qv>Pk)%Xe#B2j*kZ}7Kr@0hdU>|Y zyUti?f8ZH4T6BUFw+bJza=K=ltX_P`Lf1VQAO@SdttPqFRt)fK{kKEWjJ+t!DSE(UvQbR-=IwS8-D!*9!9!fwcthY42luk+@Z=_Cl=@si8D@S`t1ROfZ~5iU&w=Mf-m!qrha)w6N61! z0&Ppsg84R=!UrlJum`Kaz1$Ss;I}W6Gbxdu&2|9#rp8}~Jw))=iP&(>K;W+vF-Zh}orrsh;I9+0iwOQY5&MbYuM@HP+JV4dC*n3D`0GS$ zCxX9D#O~??-0D6YhrasJfD4u0YY4HY)ACc-(`k$7?zBa8ciJMlJ8co&owkVXPFqBG zr!Atp(-zU)X^ZIYv_*7x+9Ik>ud_~{YJFbL6(e=)wkORaSQ5x&*SNqaN@kK+|D#!o z>|4w}B~%76ZEG64G%Z#k`wO?t!p=hb4%UBS>{g<#YgwLZdPsP6hS$Epg(Nl0x&vDw z$amx{9TDtun<2iBJWKbJWJm<1%h^WYDF}Q*^NUlEJ<+hke8rWoD1MePJ>hXUD@s^i zcH4H8D{*HrLHKYODqyI?t&X%AZn)btFA8^j zLX#Hj4LBTkzbm%LRclBNr>)vxyXno1mGY}u4UTA&#l5NRIp16SQOIk?2m4=kLl1hDZc zud=J{e3-ZxL8P-@LgRbWj)ZISr-#H@m$@8GizoB;veKtzg(B4g(v-_)p2mXLa5IJk zv;t}6H4>^s6wJpdk+n-Gzf{qt-jMbgg{kXmv^b+j)Fo`yrG9ZlVcmDW0gPCoGC?d!nLl05SHellf4sDUU>Gl5YB{4)nSDp$wp<&#n`N8@RgK zr;#HI^L*hNEExfgUtCS9e*Pa`isq{UUK_XqnKUZVINsz|%XoTZ$C8B?^MpLr+G1GG zi+85&!RHmC9`OK#a94>|Ogu29hKmQ8i3iy@GMotrwT^(mx-9`A*Wl(I0YP)l1O#=d zz5Ywivn3i;o|fi?EKxSN16NVTH?nbY;p z)BC_r60UhZu7CFGA9FZRZ+F(%tsFfV1Aih-`?)K_VA}On0vE97w)Y%!7jjIT?boDZ z+Hq318i)Gjt+@=zVFq;@V^uKH)X6F1{euK1p4 zTKw?2@6>#?C*o&+zTeyhPIszigFx)8+;ye4KQ zb%O`B*o8&$ff~ID+o$kfGG)_`VznSO!8ZGD=4T?=K|aLn#W?N8&mdBh;p7B`n4LOt z$^Qay$-Z0agEZJajU@gV@ch-$olexNIqBme*}VnW~xG4wL6? z1!fdmQOR~IkOPFGicx9IZN-Vks#Ys-Gd1jvkAUM6uEOEz;X)#r`4`j~ZK+#R)JuVrbFQ2qSt#nE<0}q|51rzp! zH+0ny8)nkmZN8zc(otqEJ^`#Ow1}4s0c239KcruNu)dxxVTNmFnqvUN z`$9(OD$W#UjylnG?TJlM`vfT3;qRO!9F+3x57zLj%OicW3S5+}prlryvNcrVl9sU4{Bz^QX6L*iHneFjuL;GmHSViL4G*6*qO%OC;QUE{} z6Nea^@B#URsjJMVjzRr#4Xpw-#8=)6KjR;KDV3bhAI-1=)*+s4E0$^0BZ?S3P{fQ7 zZfs__59;Cv9Z5a6!QNqm4Xz0Ipx*sT&Vb&aRfQF#a;6er;=mR)e4VE$c^(0VfuQ-8c>wa%3C%Nfer<;~HF2Cn$ciwuT) zGX?_JAHT%CKq#`t-9+>+0PDvB^#N|g}GWA20{O&r%dUF0CV?^ayvy#EWdU^hPgC>n?4i){ZSs ziR5qqW!6E_sXe1VNCpgV>cirWN2v2oC-SIdUQPZVIThODEAk)@n?IqO zb##Iy#i>9m+HpyymDz)Sp>&XU>6DNt0pkwdqSF+C=hJ5wr$Wo%FaD#N_;!KcSvYUH zhT-C(0i}Vw!Cf`U$=WFU#(;QR*;X}cOTdJSmXzZQJ`PI3rRtTTOJ4kYs?IRPa0VyO zW5I4?I>&0{fi1R;h*TK9l@f~_RaF`mx7k$N@XzA)zQ6I>m~h=yfn9*Cs?%CBUv*~t zMRk(NinCFUwSfcd?&<_t8X*&^waJT+#SM(xbH=LPkYwYww$xMY{}Ra>(K84u&xp9X z#f{ir2k1N(;%RMxMKN}|I?s=IT5(ruu&K&AkcmT}9rytF#sUMCpcWZ|%bLtFqR+CT zH4Swm4`7Rdh7&HQK2cqOCHh3*ZCb?0Wwsotg5}N3VROm#SPCfCaBt;lgO_%wlBU&u z?R8q9l&P-WVo41yFZwHq5lxGvJzohxG2m-LZ{dOJygWTAfIYpf zp4|4f)5?v1Q;ZwnzC&-;p5NPj(0u6WvAqKac~wi+6=+42!~=G!T}k_^cle4WiKR_G-A=pf=~%4zBhpOc zM?*bOKYic@LTznqyvmH()c!434LNA#|5;q=TbCB9>vUPdxI!fzUz>@jVe>cf5+DlZ z7S`t0ke;AU!!LOH`|Z54Dj_%v_l<=`QZ$)T54vDXuQSp5V8#XsBv2am!F48M(8$#C zs#>fC+os@^#MaZtfBNMM?c z*S&{9)3k1WCwF-8bLa2u;<{jo7?1{ZlGY~Euoojixt{Iq28qfeO8Q=#l#ICMr#YH6 zN$E6}I=f3>Pq1yM=$KF&EX=;9uO13p`Ku`M41-Au(DF>JUEv>Dxx=-*Sv|=6gwjZT zB$XT<5A(Pq|A0Aw@il|(Krie!zp%-HiHjI)#=Hz#QlSyM=bW%){lhoAH*4B;ac1~{ z%Y2$M@$3iMR5S&TESoq-g+Q1pGH+;AKU2I6ZX*&r`#syB$ zCi33#Z%=e_rSJIsel|X26!m@Ur%JqYvvpp4$^uo<;h2QN1w;bRXa;f`H4(a*rG)e-K+Q>7HXddU*txX>m85Kl zi7_-GL|bcWsEP}NI@48sMsgKD+V@O*0oc%r?5g-EB!qv01z}%j3Wu#v4<1ka$D!Oi zJXpsgf;}&00_czJ#Ic(3RrUiYsAG2Y8$OQ8eqjFWG3yMah9l_ZTdKNZ;`jtd`i|=I zEyY^o%DU$KHJv#?HzgZ1kWWI~QYpvBJH4?Sq(^PLOTSjlV|c=kTnm{YU&oOVMZbu* z^@^9h+Lz7^&)8G4Z6>xl`LfT(5V^;H$fM2`}qO7^Yl+BZ%4>{;eyvWb$IOQLEQ*o<`G~AmsFGz-`Z85 z+Rynat=$vg7f@AA#edOe!k^A*B8}58KB7KVk4TDQC7`19-dsYVmBIWntBL zxMfawyP?|q11Vyia-_Y+U7}Xj6j#!kIC$?E9=yGA^AI&-J zcHTk?dpZk4j$5Xy1ObzvCf5Tu+t=#y3L+}u3Sqd4=h)fvn|7hEk>-%1YxJZ?k>)OP zz#@Ys`hN*ygN3PpQeM>Ji?GJw1lofhSmqRj4Snce} z&VY0@)=y@(i_(yZ0$gY-{=j7NFgQVdT)@r3mw4BsqSr)#GIO)={jQlyde`oDk;#6_ zb9<*`v5LibCqRpmT07%(+^m=iJ#C?9tUo4bTF26gBA^X&m`@Vijo<{Zow#j5*`)BnrKSFMi9ngWP4x$4yf@2T!%M z`cHHk@dPm*icZCN7~wf|>Ue@vY4cd&DZwz9{W%lATVUqdb6bL`CeE?)r1p%}qv)iD zw@HX3Y_tcipfY6V;;f#D-(4*o_oDN2f~p3xIY)TkKsqcQGL2I~Y)_0wi_RH3M%0t4uldYko??d@@~_xtr73#&qJ-{2Pg#?BTP@ zr#T>|tBtd%Nn{9z5#tdo1Y)T^>MhXf(h7fQGjZ|;Nc#XuNt}S{d;&;7R$ny}YMq%N zMR&y)Z6=6YI(n>4Q&f5HCezh@Z28!S;1$hHZo(|+5Bu<+OrD0CeBA-+jumLY#UvjrUO&Z*oHvTACgWaFAR<}AkujldFir) zSeh$WH8F^0#I++-2F|NSnowt5M>H`K6`f%-eMMTz?WOe`Tc-4I!vmH^Y2sM6<1U6% z*YhEP3@T?Bv;-#iu}o!puGjXX5y(b&6QUuB4_;}e0Y|~0q7l0W{QVjfF=HfCG804k?N8)iY0Li6G4hR z{8^s25`h;bj$^e0{b^nfl^;gtYfvnFdYP0VhOtsA4|aMFnGdFL%o%JK$?Oej%2Ez=kmv$*YPYZ`=uFAmL0zncXrh|-y&uF~WjSU}fktmS2uOz6i) zsMAeZ$V5sXR9Vw>h7>cMEDmUCxaK|VjQkBvhNdomj`TnGxyC2VqWmBo>uKGab*3<& z`NVvcl9P+ZIy+#^3hUnr5V01C5`9%I9q?oq)(sat#5MKBWzKYQAjUZ_)geZ%&;$za zp;KH5F5jC+6E7vzNqD~!@I}>=pr?Z#UQPl>syzvyG~7E)%|z3+%*^|X-zCBagn+OB zZA!2J2lNT}=_y=*nXX}NMXpB4wGL*)r7;E%IGeCS8ET z8REL5YI-}W=7{53s?Ze?r(8lDESEPnu2I;>GW~?7r~)}gsmFqW>QbEmgHMI^=4t}jqWtR& zxz=tt`o&vVh1_k4!lcz%5lGb)+Pr;z^&AjJ?RBI>&TP27>Wx96x$ilB;Kus1oPkUG zB#Ig@AwTvSy5edfI@+qE`dJ7x_E44va&}f?R0>vXxplmOuAwTTr_R+XQ%4fXP%sRf zi{vf41&mE}BfOSt_S877j3$eNZLc*T2o=(bX61`*-gP7#c#RJMM&U}@H0=z ziltxxq!Np4={F`WZWZ}YE%F}+d@#+@elZ{}1W_c;aGT!t2`6fIp-OhS+OJ?6I7@ZR z~3jIA7239O|5b<9|?62f0eqTN&jt^%VI0>GKF z))|V5aT9wEO~ROp;_p)iemVEN{9PHCdo{K?EjTsKGr8b_ zT>wbK!wXGYodl`qgO(0*oCg*2^Je{o>yMl#2R2;F@g}3nJLlBFQCE3=TI|cxzV*+8 ztwH%>Ywe5eb}JtiV@sQ6PWOHJKZ9E8@Cbe+1aWQNXMK-(e?7&lpGA5g&qc|0>zxX* z@|$d3V%O|pYVppy7*g5DAr7;W)&+U8YROzmGjp;XX&ezwge=$2#f)4S%tny8Xq-pY zXj=z66?2u0sP-hOE<|vo%iQOp9dCPAJUg_qG)n1tvfIibP!|^BW_F&fVH1;aw(?jS z<)LuE-V2vF!aK`$pAr_(FbiqO5q4oDYkunC&jN}y*I$@E@Eu89f||1D8rOzd=Ekmw zIggej(`FkLU5z0yKF&R@@0qM7J`ZvG{cZ`%@~!3Gz6k=R`F)hdiodV{4B9s&(oj$d4wKAf%hY$( zQ@C>2^`Jt^Zpp1w4ngQWqsS5>_=qbhV+dk!A=NFmq)YVGk9qYAv4# zPe1eV^$1Vkv*%M0M_-Pk7>Fl1`%=|2>G+lj$R!N5KZ8~NsqZPcoEjDY{yH|3h*trz z32ZLM)3$!kXZ1%PN35nNUr>!g%gnM&O7AB^b9u-mh>jJhq#aK}heGnDsK1+-pCYHBCEtY8>V~2>fYo+A1VGvZK zh#1-sS>86Rqc~{_{V|sNII#w8(s8jS#e2Z{R*aQ}X)owuKs=~Bk06>`pCFpswji1d zry!c!CqXo~|9!;H#TDbnIxN^bSk)q_k@9I&bxHDuxZIZW%*fSWAzDUBSNvJC-!qTKim0i`r2mFYM3bZh+DK! zuL(0&;mYia;8s=Yuy+ao>5`7fHl`y|LtmY8;)NC`sxd6vh2f9IgEB0K+EVwbB?#_zEoGv}c6~Z9Iil?!C+Kqvb{`zKrVl z-|<%$1=v#qrf(7exW8g;f9y>MfBGRZY$1xQvmT#$~d^((A#Zr=#AhCQ1H1_JY- z=Id$BY#_g{bM5)SpfVHkx-AKk#9Z~)nF$cK%>=o3lr(eN0#uG2*tC@0hyVRR(Qrgz6y$*_0O94}ABI%2! z#=#M++ax zm@5M32kS(sCLESoM+!0d?hf$Z! zj7crywuWy{U%vzq5Qd0H1?}X+k!LJE)#J^b5|BdEeQ<2%`ZmvT-vqi+nzRx)I=@*FWZA#3(jOB zME^;scJM}&$_;8!4*!T%+gtgH#=9WrrGnVTViG1 z0P@;3_|)7}x#iJhv8g@9F{5NhA?NG-RJj*o*pAgr3ym03KkTdT;*hV=^_?Fw)t+p_ zrWQg*QLqT?uml8Y>xCcdB??~{o*gB#!M35UAgPdY2)L_k&j#@G>(=wjRmiSJ%MG3V zU!wCx@!s^mC!uEJB9G;4**YZP+=Ue=TA^2~tFXrO@s;g=1X#l_9IQ1U%SN}ev_2R8 z+N6tR;>4qhiZUw#uBIkZY7v%|ze1uZewen6?tL|VUh~Cja<*?%?%7#M1WYohwvEP% z7>A1E6Eu!5CZO72zsl3KpqaA|9hqfPXOX7}#~AopXTyn1e!AN5^#{Hp&Vos?bBp`a z;@K{3%i3 zZ!&^iv+LQ&udRVv$cj0ROkvlDQIG!0riEIz6lsR)sdt>-ui_iAK;I@JoWnN(19W9} z6U6r?xoN;;sDE67(Rp>0sVpXcE-Zgx`AyDPHPMbg$ASyPfPCpnnZm~ZTqgxYFm3nV z%q-2x$PcDcb8hjlq0Q1+K#`=S6Z(jVrGzQtFj| zptpBy62@H7_lgqE5qt(N+)bbw*XSoW~SV^Oxv=O!$2TeKc#ED)neQQ0P z(1i=jP>P^;Njut`0l(#8!!EFiYSnLvMLKycibzv>*U`1$k*Hz$C=X?_wImrIYp>;w zh;KB$Eh|d_-%&dYAcM_-mr7ZCW!pkTUGo~VVG%N-hPh!R#dBt1V}waS0|B#-%xnXs z&2+E(^h2U{W-k1->(Pg~?6()t=&m`j@5v@zS38>sxNlW+0L5s|S!W{!{DIMguxh)C z*RiBK&|X{HH|@270xbu-LF?BQwgUipLW;B_ly?=aI{dsAJE-B=#Sj{jq7|A6@66c0 zX{=}tQG&ybs%hi>MjV|SVo^3qHi*W&&eH}9n!)1)-C@A55L#DeYhe92Z9S9j@tENs0&Hu-jsgug|FDw z3_WTyDU9CD^^-FxW9^nv2LzU}&8LjbTN-C-aH8_3uzG{(dmWgNj^i%l1Xw6bb z#PQUlg`uX^g5keO^c1lQPOk76VXAmaU^%L^*VA-<-Xx-o6pGC7IV9wW_auDNaY9iw z6&Au|ydtJWYZ8|Y#<*AUYR0+U188&-2Z?s;hZrXoYc0WojOAj^P?2maa`<9WK1%oh zh&A9h>^&_!=LF%PSM>R4FOnsCi2S7GdVc7(wKQU>-RSK7NW;605{62RnMic0^N~UM zu%sbLPnM=o6YL9*Fr9HYk>@1lH(slY$Npz^lL`2Iy4muhH)Z{A>gR{#5-}T( z24Zf@Se?yHdk7D`sf;>N4IEEXYq*SS+Hm2thtr@sWX>$N;mX^yfx7@*TuY)=cdj6B5k~o3_L_!n6nmWd`!Q=ozl_%_H2!y2GtoT8l+P-IR#|$m zWFrW<%qM_~S8<%7#VoySwR(jJ3cjtPj^t_5<}RNc)safpK2ouD53Q$;*v+)ye4gB3 zsYA)C^Apd=s>cerP)~oUnUS#PpvhTI$Kxo@c_#EC+LZ^Xu3q+%gMr9t6IHeQ=wMT_ zzB(Lky9>1&iT3+8EG&~S>djwIVAU+hCbY4Ja2=?;Bl1w63x1#hOwT1@bog0ZJRp+U z?9B2QD=Y7$mD7Mht;9nj|FG%M(kqzoKBaoZ6>pstQJYX5e`HHtHtzpZb|%kBFve4` z4@FeZbXzjT9aUZ(_ngvm-1F+^bKK)%iG-kHQQTnA%CflV+li3L28J<)UCU6b50e`| zgo}<1;^jb{-9r~)fUAD-@m*dfv;jCD${3s&<1n%FpEUFI>ikC5zEX&_V;Jm|<{4@2 zhZe|4)}Q;e16Ex|_4*rWg`aG^onvCGmZVuOlMfY@s7gf~jZ^Ug)c=-nkec3i^Q26~@U>N!K(?-Ns%iXvyPQC}M57BVBCfN#O%_JeuPp zOHndym^BEPFoU_-Wj&*C$pK_CD~oKpRM(tRoMOI8vGN-4((2AAts+~S^!mW8?{9Rz zM-mZQ0;JG+^)a9iIiOKWKOdkmEw9SVR6cClRY!DsETs79MbwvjGr0i~!(5zXAX*2LR-MGmr=XposwBtB q*W$N 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

?`n)grlcD=PCPP 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ðQ~$&04{tdb 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

^o_bG*yjr!yGe(rwv;Z(Xft?6vm4a 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 r*f0UW{+9K#8m!Wo>y1zf@v{DW(_ft$FE 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

WtF02Kwr~g?M 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 literal 0 HcmV?d00001 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index dee51e92..3334c0e4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1 @@ -packages: - - "packages/*" +packages: [] diff --git a/src/main/agent-role-helpers.ts b/src/main/agent-role-helpers.ts index 13f567e0..0104dbe6 100644 --- a/src/main/agent-role-helpers.ts +++ b/src/main/agent-role-helpers.ts @@ -1,11 +1,14 @@ /** * Role-classification helpers used across prompt construction and skill - * auto-assignment. Keeping a single keyword list here prevents drift - * between system-prompt.ts and automation-skills.ts. + * auto-assignment. Historical keyword match remains here for backfill and + * UI-free fallback; typed `agent.roleLevel` is the source of truth once the + * column is populated. * * Matching uses word-boundary tokenization instead of substring search, * so "lead" does not match "cleaner" or "leaderboard". */ +import type { AgentRoleLevel } from "@shared/types"; +import { isLeadershipLevel } from "@shared/types"; export const LEADERSHIP_ROLE_KEYWORDS = [ "chief", @@ -31,3 +34,26 @@ export function roleMatchesLeadershipKeywords(role: string, title?: string | nul const tokens = new Set(tokenize(`${role} ${title ?? ""}`)); return LEADERSHIP_ROLE_KEYWORDS.some((keyword) => tokens.has(keyword)); } + +/** + * One-shot inference from free-text role/title strings. Used when creating or + * backfilling an agent so `roleLevel` can be stored once and re-read cheaply. + * The most specific token wins; order here is most-senior → least. + */ +export function inferRoleLevel(role: string, title?: string | null): AgentRoleLevel { + const tokens = new Set(tokenize(`${role} ${title ?? ""}`)); + if (tokens.has("ceo") || tokens.has("cto") || tokens.has("cmo") || tokens.has("coo") || tokens.has("cfo") || tokens.has("chief")) { + return "c_suite"; + } + if (tokens.has("vp") || tokens.has("vice") || tokens.has("president")) return "vp"; + if (tokens.has("director") || tokens.has("head")) return "director"; + if (tokens.has("manager")) return "manager"; + if (tokens.has("lead")) return "lead"; + return "ic"; +} + +/** Canonical leadership check: typed enum wins, fallback to keyword match. */ +export function agentIsLeader(agent: { roleLevel?: AgentRoleLevel | null; role: string; title: string | null }): boolean { + if (agent.roleLevel) return isLeadershipLevel(agent.roleLevel); + return roleMatchesLeadershipKeywords(agent.role, agent.title); +} diff --git a/src/main/agent-runtime/approval-hook.ts b/src/main/agent-runtime/approval-hook.ts new file mode 100644 index 00000000..8d54fdf0 --- /dev/null +++ b/src/main/agent-runtime/approval-hook.ts @@ -0,0 +1,134 @@ +import type { AppDatabase } from "@main/database"; +import type { DesktopEvent } from "@shared/types"; + +interface BeforeRequestPayload { + agentId: string; + companyId: string; + taskId: string | null; + toolCallId: string; + toolName: string; + args: unknown; +} + +interface AfterRequestPayload { + toolCallId: string; + toolName: string; + isError: boolean; +} + +export interface HumanApprovalHookDeps { + db: AppDatabase; + emitEvent: (event: DesktopEvent) => void; + logger: { + info: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; + }; +} + +interface PendingApproval { + approvalId: string; + resolve: (verdict: { block?: boolean; reason?: string }) => void; +} + +const TOOL_LABEL: Record = { + bash: "shell", + edit: "edit", + write: "write", + read: "read", + ls: "ls", + grep: "grep", + find: "find", +}; + +function summariseArgs(toolName: string, args: unknown): string { + if (args === null || typeof args !== "object") return String(args); + const argsObj = args as Record; + switch (toolName) { + case "bash": { + const command = typeof argsObj.command === "string" ? argsObj.command : ""; + return command.length > 200 ? `${command.slice(0, 197)}...` : command; + } + case "write": + case "edit": + case "read": { + const path = typeof argsObj.path === "string" ? argsObj.path : (typeof argsObj.file_path === "string" ? argsObj.file_path : ""); + return path; + } + default: + try { + const json = JSON.stringify(args); + return json.length > 200 ? `${json.slice(0, 197)}...` : json; + } catch { + return ""; + } + } +} + +/** + * Human-in-the-loop hook bridging pi `beforeToolCall` to + * AgentCompany's `ApprovalService` via the existing approvals table. + */ +export class HumanApprovalHook { + private readonly pending = new Map(); + + constructor(private readonly deps: HumanApprovalHookDeps) {} + + async before(runId: string, payload: BeforeRequestPayload): Promise<{ block?: boolean; reason?: string }> { + const { db, emitEvent } = this.deps; + const summary = summariseArgs(payload.toolName, payload.args); + const label = TOOL_LABEL[payload.toolName] ?? payload.toolName; + const approvalId = db.requestApproval({ + companyId: payload.companyId, + relatedTaskId: payload.taskId, + requestedByAgentId: payload.agentId, + type: "dangerous_command", + payloadSummary: `${label}: ${summary}`.slice(0, 280), + impactSummary: `Agent ${payload.agentId} requested ${label} during run ${runId}.`, + payloadJson: JSON.stringify({ + runId, + toolCallId: payload.toolCallId, + toolName: payload.toolName, + args: payload.args, + }), + }); + + emitEvent({ type: "approval-updated", approvalId, action: "created" }); + emitEvent({ type: "domain-changed" }); + + return new Promise<{ block?: boolean; reason?: string }>((resolve) => { + this.pending.set(approvalId, { approvalId, resolve }); + }); + } + + /** + * Called by the IPC approval-decided pipeline when a user resolves a + * pending approval. If we have a pending pi `beforeToolCall` waiting on + * this approval, resolve it. + */ + resolveApproval(approvalId: string, decision: "approve" | "reject" | "revision_requested", note: string): boolean { + const pending = this.pending.get(approvalId); + if (!pending) return false; + this.pending.delete(approvalId); + if (decision === "approve") { + pending.resolve({}); + } else { + pending.resolve({ block: true, reason: note || `Tool execution ${decision}.` }); + } + return true; + } + + /** Called when a run is cancelled — resolves any pending approvals as blocked. */ + cancelAll(reason: string): void { + for (const [id, pending] of this.pending) { + pending.resolve({ block: true, reason }); + this.pending.delete(id); + } + } + + + async after(_runId: string, _payload: AfterRequestPayload): Promise { + // Audit hook intentionally minimal — actual tool result is captured by + // the agent-event stream and persisted via SqliteSessionStore. + } +} diff --git a/src/main/agent-runtime/index.ts b/src/main/agent-runtime/index.ts new file mode 100644 index 00000000..bce9d9d3 --- /dev/null +++ b/src/main/agent-runtime/index.ts @@ -0,0 +1,4 @@ +export { SqliteSessionStore } from "./sqlite-session-store"; +export { HumanApprovalHook } from "./approval-hook"; +export { CostWriter } from "./cost-writer"; +export { RuntimeBridge } from "./runtime-bridge"; diff --git a/src/main/agent-runtime/sqlite-session-store.ts b/src/main/agent-runtime/sqlite-session-store.ts new file mode 100644 index 00000000..d6acbde2 --- /dev/null +++ b/src/main/agent-runtime/sqlite-session-store.ts @@ -0,0 +1,65 @@ +import type { AgentMessage } from "@mariozechner/pi-agent-core"; +import type { AppDatabase } from "@main/database"; + +/** + * SQLite-backed pi `AgentMessage[]` store. + * + * One store instance per task_session. Owners must call `appendMessage` + * exactly once per pi `message_end` event, and `replaceMessages` after pi + * `compact` returns a compacted message list. + */ +export class SqliteSessionStore { + constructor( + private readonly db: AppDatabase, + public readonly taskSessionId: string, + ) {} + + loadMessages(): AgentMessage[] { + const rows = this.db.sessionEntries.listActive(this.taskSessionId); + const result: AgentMessage[] = []; + for (const row of rows) { + if (row.kind === "message") { + try { + result.push(JSON.parse(row.payloadJson) as AgentMessage); + } catch { + // Skip malformed entries (defensive) — preserves the rest of the + // transcript instead of failing the entire load. + } + } + } + return result; + } + + appendMessage(message: AgentMessage): void { + this.db.sessionEntries.append({ + taskSessionId: this.taskSessionId, + kind: "message", + role: (message as { role?: string }).role ?? null, + payloadJson: JSON.stringify(message), + }); + } + + appendCompaction(summary: string): void { + this.db.sessionEntries.append({ + taskSessionId: this.taskSessionId, + kind: "compaction", + role: null, + payloadJson: JSON.stringify({ summary }), + }); + } + + replaceMessages(messages: AgentMessage[], compactionSummary?: string): void { + const entries: Array<{ kind: "message" | "compaction"; role: string | null; payloadJson: string }> = []; + if (compactionSummary) { + entries.push({ kind: "compaction", role: null, payloadJson: JSON.stringify({ summary: compactionSummary }) }); + } + for (const message of messages) { + entries.push({ + kind: "message", + role: (message as { role?: string }).role ?? null, + payloadJson: JSON.stringify(message), + }); + } + this.db.sessionEntries.replace({ taskSessionId: this.taskSessionId, entries }); + } +} diff --git a/src/main/credentials/credential-store.ts b/src/main/credentials/credential-store.ts new file mode 100644 index 00000000..32c12da3 --- /dev/null +++ b/src/main/credentials/credential-store.ts @@ -0,0 +1,122 @@ +import type { AppDatabase } from "@main/database"; +import type { CredentialBackend, CredentialKind } from "@main/database/credentials"; +import type { CredentialStatus } from "@main/database/credentials"; +import type { CredentialSummary } from "@shared/contracts/schemas-credentials"; +import type { StoredCredentialPayload } from "./types"; + +const HINT_SUFFIX_LEN = 4; +const HINT_PREFIX_LEN = 6; + +function maskApiKey(value: string): string { + if (value.length <= HINT_PREFIX_LEN + HINT_SUFFIX_LEN) { + return `${value.slice(0, 2)}...`; + } + const prefix = value.slice(0, HINT_PREFIX_LEN); + const suffix = value.slice(-HINT_SUFFIX_LEN); + return `${prefix}...${suffix}`; +} + +export function deriveDisplayHint(payload: StoredCredentialPayload): string { + switch (payload.kind) { + case "api_key": + return payload.value.source === "literal" + ? maskApiKey(payload.value.value) + : `env:${payload.value.value}`; + case "azure": + return payload.value.resourceName + ? `azure:${payload.value.resourceName}` + : `azure:${maskApiKey(payload.value.apiKey)}`; + case "aws": + if (payload.value.authMode === "profile") return `aws:profile=${payload.value.awsProfile ?? "?"}`; + if (payload.value.authMode === "iam") return `aws:iam=${maskApiKey(payload.value.accessKeyId ?? "?")}`; + return `aws:bearer`; + case "gcp_adc": + return `gcp:${payload.value.project}@${payload.value.location}`; + case "oauth": { + const expiresMs = payload.value.expires; + const expiresLabel = typeof expiresMs === "number" && Number.isFinite(expiresMs) + ? new Date(expiresMs).toISOString() + : "no-expiry"; + return `oauth:expires=${expiresLabel}`; + } + } +} + +export interface CredentialStoreDeps { + db: AppDatabase; +} + +export class CredentialStore { + constructor(private readonly deps: CredentialStoreDeps) {} + + list(): CredentialSummary[] { + return this.deps.db.credentialsTable.list().map((row) => ({ + connectorId: row.connectorId, + kind: row.kind as CredentialKind, + displayHint: row.displayHint, + status: row.status, + expiresAt: row.expiresAt, + lastRefreshedAt: row.lastRefreshedAt, + lastError: row.lastError, + updatedAt: row.updatedAt, + })); + } + + /** Encrypts and stores a credential. Caller is responsible for kind/connectorId match. */ + save(connectorId: string, payload: StoredCredentialPayload, expiresAt: string | null = null): CredentialSummary { + const json = JSON.stringify(payload.value); + const { backend, payload: ciphertext } = this.deps.db.vault.encrypt(json); + const stored = this.deps.db.credentialsTable.upsert({ + connectorId, + kind: payload.kind, + backend: backend as CredentialBackend, + ciphertext, + displayHint: deriveDisplayHint(payload), + status: "active", + expiresAt, + lastRefreshedAt: null, + lastError: null, + }); + return { + connectorId: stored.connectorId, + kind: stored.kind, + displayHint: stored.displayHint, + status: stored.status, + expiresAt: stored.expiresAt, + lastRefreshedAt: stored.lastRefreshedAt, + lastError: stored.lastError, + updatedAt: stored.updatedAt, + }; + } + + /** Decrypts and returns the typed credential payload. Returns null when missing. */ + read(connectorId: string): StoredCredentialPayload | null { + const row = this.deps.db.credentialsTable.get(connectorId); + if (!row) return null; + const json = this.deps.db.vault.decrypt( + row.backend, + Buffer.isBuffer(row.ciphertext) ? row.ciphertext : Buffer.from(row.ciphertext), + ); + const value = JSON.parse(json); + return { kind: row.kind, value } as StoredCredentialPayload; + } + + remove(connectorId: string): void { + this.deps.db.credentialsTable.remove(connectorId); + } + + setStatus(connectorId: string, status: CredentialStatus, lastError: string | null = null): void { + this.deps.db.credentialsTable.setStatus(connectorId, status, lastError); + } + + /** Replaces the OAuth ciphertext after a successful refresh. */ + saveRefreshedOAuth(connectorId: string, payload: StoredCredentialPayload, expiresAt: string | null): void { + const { backend, payload: ciphertext } = this.deps.db.vault.encrypt(JSON.stringify(payload.value)); + this.deps.db.credentialsTable.markRefreshed( + connectorId, + backend as CredentialBackend, + ciphertext, + expiresAt, + ); + } +} diff --git a/src/main/credentials/index.ts b/src/main/credentials/index.ts new file mode 100644 index 00000000..a886f453 --- /dev/null +++ b/src/main/credentials/index.ts @@ -0,0 +1,4 @@ +export { CredentialStore, deriveDisplayHint } from "./credential-store"; +export { CredentialResolver, CredentialError, type ResolvedCredential } from "./credential-resolver"; +export { OAuthLoginService } from "./oauth-login-service"; +export type { StoredCredentialPayload, OAuthCredentialPayload, CredentialKind } from "./types"; diff --git a/src/main/credentials/types.ts b/src/main/credentials/types.ts new file mode 100644 index 00000000..aaa6e7b9 --- /dev/null +++ b/src/main/credentials/types.ts @@ -0,0 +1,30 @@ +import type { + ApiKeyValue, + AwsCredential, + AzureCredential, + GcpAdcCredential, +} from "@shared/contracts/schemas-credentials"; + +/** + * pi-ai's `OAuthCredentials` shape. + * + * Mirrored locally as a type-only declaration to avoid pulling pi runtime + * symbols into modules that only need the shape. The actual contract is + * enforced by pi at runtime via the OAuth provider interface. + */ +export interface OAuthCredentialPayload { + refresh: string; + access: string; + expires: number; + /** Provider-specific extra fields (e.g. github copilot domain) */ + [key: string]: unknown; +} + +export type StoredCredentialPayload = + | { kind: "oauth"; value: OAuthCredentialPayload } + | { kind: "api_key"; value: ApiKeyValue } + | { kind: "azure"; value: AzureCredential } + | { kind: "aws"; value: AwsCredential } + | { kind: "gcp_adc"; value: GcpAdcCredential }; + +export type CredentialKind = StoredCredentialPayload["kind"]; diff --git a/src/main/database-stale-recovery.ts b/src/main/database-stale-recovery.ts new file mode 100644 index 00000000..6202d481 --- /dev/null +++ b/src/main/database-stale-recovery.ts @@ -0,0 +1,70 @@ +import type { DatabaseSync } from "node:sqlite"; +import { STALE_LOCK_TIMEOUT_MS } from "./database-base"; + +function nowIso(): string { + return new Date().toISOString(); +} + +/** + * Stale-state recovery helpers extracted from `database-base.ts` to keep that + * file under the 400-line cap. Both helpers are called by `AppDatabase.init` + * (interruptStaleRuns) and the orchestration recovery loop (recoverStaleLocks). + */ + +interface InterruptDeps { + db: DatabaseSync; + transaction: (op: () => T) => T; + releaseAllWorkspaceLocks: () => void; +} + +export function interruptStaleRunsImpl(deps: InterruptDeps): number { + const ts = nowIso(); + let interruptedCount = 0; + deps.transaction(() => { + const result = deps.db + .prepare( + "update runs set status = 'interrupted', error_message = 'AgentCompany restarted before the run could report completion.', finished_at = ?, updated_at = ? where status = 'running'", + ) + .run(ts, ts); + interruptedCount = Number(result.changes); + if (interruptedCount > 0) { + deps.db + .prepare( + "update tasks set status = 'blocked', updated_at = ? where id in (select task_id from runs where status = 'interrupted' and finished_at = ?) and status in ('in_progress', 'todo')", + ) + .run(ts, ts); + } + deps.db + .prepare( + "update tasks set active_run_id = null where active_run_id in (select id from runs where status = 'interrupted')", + ) + .run(); + deps.db.prepare("update agents set status = 'idle', updated_at = ? where status = 'running'").run(ts); + deps.releaseAllWorkspaceLocks(); + }); + return interruptedCount; +} + +interface RecoverDeps { + db: DatabaseSync; + readAll: (sql: string, ...params: import("node:sqlite").SQLInputValue[]) => T[]; +} + +export function recoverStaleLocksImpl( + deps: RecoverDeps, +): Array<{ taskId: string; companyId: string; title: string }> { + const cutoff = new Date(Date.now() - STALE_LOCK_TIMEOUT_MS).toISOString(); + const stale = deps.readAll<{ id: string; companyId: string; title: string }>( + "SELECT id, company_id AS companyId, title FROM tasks WHERE execution_locked_at IS NOT NULL AND execution_locked_at < ? AND status = 'in_progress' AND checkout_run_id NOT IN (SELECT id FROM runs WHERE status = 'running')", + cutoff, + ); + const ts = nowIso(); + for (const task of stale) { + deps.db + .prepare( + "UPDATE tasks SET checkout_run_id = NULL, execution_locked_at = NULL, status = 'todo', updated_at = ? WHERE id = ?", + ) + .run(ts, task.id); + } + return stale.map((t) => ({ taskId: t.id, companyId: t.companyId, title: t.title })); +} diff --git a/src/main/database/entity-crud.ts b/src/main/database/entity-crud.ts index 5e124130..46ec06e4 100644 --- a/src/main/database/entity-crud.ts +++ b/src/main/database/entity-crud.ts @@ -63,7 +63,7 @@ export function buildEntityCrudMethods( getAgent(id: string): AgentRecord { const row = getDb() .prepare( - "select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents where id = ?", + "select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, role_level as roleLevel, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents where id = ?", ) .get(id) as AgentRecord | undefined; if (!row) throw new Error(`Agent not found: ${id}`); @@ -74,7 +74,7 @@ export function buildEntityCrudMethods( findAgent(id: string): AgentRecord | null { const row = getDb() .prepare( - "select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents where id = ?", + "select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, role_level as roleLevel, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents where id = ?", ) .get(id) as AgentRecord | undefined; if (!row) return null; diff --git a/src/main/database/snapshot.ts b/src/main/database/snapshot.ts index 6d06e662..72907140 100644 --- a/src/main/database/snapshot.ts +++ b/src/main/database/snapshot.ts @@ -63,7 +63,7 @@ export function buildSnapshotMethods( ...companyArgs, ).map((record) => ({ ...record, isPrimary: toBoolean(record.isPrimary) })) as WorkspaceRecord[], agents: readAll( - `select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents${companyWhere} order by created_at asc`, + `select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, role_level as roleLevel, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents${companyWhere} order by created_at asc`, ...companyArgs, ).map((record: SqliteRow) => ({ ...record, heartbeatEnabled: toBoolean(record.heartbeatEnabled) })) as AgentRecord[], goals: readAll( diff --git a/src/main/model-resolver.ts b/src/main/model-resolver.ts new file mode 100644 index 00000000..1c90fa5c --- /dev/null +++ b/src/main/model-resolver.ts @@ -0,0 +1,91 @@ +/** + * Resolves `Model` and the dynamic stream headers needed for providers + * that pi-ai's catalog does not ship out of the box. + * + * Today this only covers the two Fireworks connectors (`fireworks` and + * `fireworks-firepass`), which both speak the Anthropic-compatible endpoint + * at `https://api.fireworks.ai/inference` and require Bearer auth. For every + * other provider we delegate to pi-ai's `getModel` / `getModels`. + */ + +import { getModel, getModels, type Api, type Model } from "@mariozechner/pi-ai"; + +const FIREWORKS_BASE_URL = "https://api.fireworks.ai/inference"; +const FIREWORKS_DEFAULT_CONTEXT = 128_000; +const FIREWORKS_DEFAULT_MAX_TOKENS = 8192; + +export const FIREWORKS_FIREPASS_MODEL_ID = "accounts/fireworks/routers/kimi-k2p5-turbo"; +export const FIREWORKS_FIREPASS_MODEL_NAME = "Kimi K2.5 Turbo (Fire Pass)"; + +const FIREWORKS_CURATED_MODELS: Array<{ id: string; name: string; contextWindow?: number }> = [ + { id: "accounts/fireworks/models/kimi-k2-instruct", name: "Kimi K2 Instruct", contextWindow: 131_072 }, + { id: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct", name: "Qwen3 Coder 480B A35B Instruct", contextWindow: 262_144 }, + { id: "accounts/fireworks/models/deepseek-v3p1", name: "DeepSeek V3.1", contextWindow: 163_840 }, + { id: "accounts/fireworks/models/glm-4p5", name: "GLM 4.5", contextWindow: 131_072 }, + { id: "accounts/fireworks/models/llama-v3p3-70b-instruct", name: "Llama 3.3 70B Instruct", contextWindow: 131_072 }, +]; + +function buildFireworksModel(modelId: string, name?: string, contextWindow?: number): Model<"anthropic-messages"> { + return { + id: modelId, + name: name ?? modelId, + api: "anthropic-messages", + provider: "fireworks", + baseUrl: FIREWORKS_BASE_URL, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: contextWindow ?? FIREWORKS_DEFAULT_CONTEXT, + maxTokens: FIREWORKS_DEFAULT_MAX_TOKENS, + }; +} + +function isFireworksProvider(provider: string): boolean { + return provider === "fireworks" || provider === "fireworks-firepass"; +} + +export function resolveModel(provider: string, modelId: string): Model | null { + if (provider === "fireworks-firepass") { + return buildFireworksModel(FIREWORKS_FIREPASS_MODEL_ID, FIREWORKS_FIREPASS_MODEL_NAME, 262_144); + } + if (provider === "fireworks") { + const curated = FIREWORKS_CURATED_MODELS.find((entry) => entry.id === modelId); + return buildFireworksModel(modelId, curated?.name, curated?.contextWindow); + } + try { + return getModel(provider as never, modelId as never) ?? null; + } catch { + return null; + } +} + +export function resolveModels(provider: string): Array<{ id: string; name: string }> { + if (provider === "fireworks-firepass") { + return [{ id: FIREWORKS_FIREPASS_MODEL_ID, name: FIREWORKS_FIREPASS_MODEL_NAME }]; + } + if (provider === "fireworks") { + return FIREWORKS_CURATED_MODELS.map(({ id, name }) => ({ id, name })); + } + try { + const models = getModels(provider as never) ?? []; + return models.map((model: { id: string; name?: string }) => ({ id: model.id, name: model.name ?? model.id })); + } catch { + return []; + } +} + +/** + * Extra HTTP headers that must be merged into the streamSimple call for the + * given provider. Returns undefined when no extra headers are required. + * + * Fireworks' Anthropic-compat endpoint authenticates via `Authorization: Bearer`, + * not `x-api-key`. pi-ai's anthropic provider only sends `x-api-key`, so we + * inject the bearer header at the call site. + */ +export function resolveExtraStreamHeaders(provider: string, apiKey: string): Record | undefined { + if (!apiKey) return undefined; + if (isFireworksProvider(provider)) { + return { Authorization: `Bearer ${apiKey}` }; + } + return undefined; +} diff --git a/src/main/resume-interrupted-tasks.ts b/src/main/resume-interrupted-tasks.ts new file mode 100644 index 00000000..2e4b07da --- /dev/null +++ b/src/main/resume-interrupted-tasks.ts @@ -0,0 +1,65 @@ +import type log from "electron-log/main.js"; +import type { AppDatabase } from "./database"; + +interface ResumeInterruptedTasksDeps { + db: AppDatabase; + logger: typeof log; + wakeAgentIfPossible: (agentId: string, companyId: string, trigger: string) => string | null; + publishDomainChanged: () => void; +} + +/** + * Resume tasks that were left behind by a clean shutdown. + * + * The worker-service marks running/queued runs as "interrupted" with a fixed + * summary string on shutdown, but does not reset the owning task. This helper + * rolls those tasks back to "todo", releases their run locks, and re-wakes the + * assigned agents so heartbeat can pick them up again. + */ +export function resumeInterruptedTasks({ + db, + logger, + wakeAgentIfPossible, + publishDomainChanged, +}: ResumeInterruptedTasksDeps): void { + try { + const snapshot = db.listSnapshot(); + const shutdownRuns = snapshot.runs.filter( + (r) => + r.status === "interrupted" && + (r.summary === "Application shut down." || r.summary === "Worker process crashed."), + ); + if (shutdownRuns.length === 0) return; + + const resumedAgents = new Set(); + for (const run of shutdownRuns) { + try { + const task = db.getTask(run.taskId); + if (task.status === "in_progress" || task.status === "blocked") { + db.advanceTaskStatus(run.taskId, "todo", "Auto-resumed after app restart."); + db.releaseTaskLock(run.taskId, run.id); + } + if (!resumedAgents.has(run.agentId)) { + resumedAgents.add(run.agentId); + wakeAgentIfPossible(run.agentId, run.companyId, "app_restart_resume"); + } + } catch (error) { + logger.warn( + `[bootstrap] Skipped shutdown recovery for run ${run.id} (task/agent may have been deleted):`, + error, + ); + } + } + + if (resumedAgents.size > 0) { + logger.info( + `[bootstrap] Resumed ${shutdownRuns.length} interrupted task(s) across ${resumedAgents.size} agent(s) after clean shutdown recovery.`, + ); + publishDomainChanged(); + } + } catch (err) { + logger.warn( + `[bootstrap] Shutdown recovery failed: ${err instanceof Error ? err.message : String(err)}`, + ); + } +} diff --git a/src/main/service-graph-orchestration-deps.ts b/src/main/service-graph-orchestration-deps.ts new file mode 100644 index 00000000..70b83b6a --- /dev/null +++ b/src/main/service-graph-orchestration-deps.ts @@ -0,0 +1,65 @@ +/** + * Helpers for assembling orchestration-service dependencies in service-graph.ts. + * Extracted to keep service-graph.ts under the 400-line cap. + */ + +import type { AppDatabase } from "./database"; +import { createSkillService } from "./skill-service"; +import type { createSessionManagementService } from "./session-management-service"; +import type { RuntimeSkillEntry } from "./skill-types"; + +interface Logger { + info: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; +} + +export function makeListRuntimeSkillEntries(deps: { db: AppDatabase; logger: Logger }) { + let cache: ReturnType | null = null; + return (companyId: string, agentId: string, connectorId: string): RuntimeSkillEntry[] => { + try { + if (!cache) cache = createSkillService({ db: deps.db, profileDir: deps.db.profileDir, logger: deps.logger }); + return cache.listRuntimeSkillEntries(companyId, agentId, connectorId); + } catch (error) { + deps.logger.warn(`[service-graph] listRuntimeSkillEntries failed for agent ${agentId}:`, error); + return []; + } + }; +} + +export function makeSessionHandoffLookup( + deps: { logger: Logger }, + getSessionService: () => ReturnType, +) { + return (taskId: string): string | null => { + try { + return getSessionService().getHandoffContext(taskId); + } catch (error) { + deps.logger.warn(`[service-graph] getSessionHandoff failed for task ${taskId}:`, error); + return null; + } + }; +} + +export function makePreRunRotationEvaluator( + deps: { db: AppDatabase; logger: Logger }, + getSessionService: () => ReturnType, +) { + return (taskId: string, connectorId: string): { rotated: boolean; handoff: string | null } => { + try { + const sessionSvc = getSessionService(); + const activeSession = deps.db.getActiveTaskSession(taskId); + if (!activeSession) return { rotated: false, handoff: null }; + const result = sessionSvc.evaluateRotation(activeSession, connectorId); + if (result.shouldRotate) { + sessionSvc.rotateSession(activeSession); + const handoff = sessionSvc.getHandoffContext(taskId); + return { rotated: true, handoff }; + } + return { rotated: false, handoff: null }; + } catch (error) { + deps.logger.warn(`[service-graph] evaluatePreRunSessionRotation failed for task ${taskId}:`, error); + return { rotated: false, handoff: null }; + } + }; +} diff --git a/src/main/service-graph-pi-runtime.ts b/src/main/service-graph-pi-runtime.ts new file mode 100644 index 00000000..407e0ab0 --- /dev/null +++ b/src/main/service-graph-pi-runtime.ts @@ -0,0 +1,58 @@ +/** + * Pi-mono runtime + credential service factory. + * + * Composes `CredentialStore`, `CredentialResolver`, `OAuthLoginService`, and + * `RuntimeBridge` for `service-graph.ts`. Extracted to keep `service-graph.ts` + * under the 400-line cap. + */ + +import type { AppDatabase } from "./database"; +import type { DesktopEvent } from "@shared/types"; +import { CredentialResolver, CredentialStore, OAuthLoginService } from "./credentials"; +import { RuntimeBridge } from "./agent-runtime"; + +export interface PiRuntimeServicesDeps { + db: AppDatabase; + emitEvent: (event: DesktopEvent) => void; + refreshConnectorHealth: (connectorId: string) => Promise; + logger: { + info: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; + }; +} + +export interface PiRuntimeServices { + credentialStore: CredentialStore; + credentialResolver: CredentialResolver; + oauthLoginService: OAuthLoginService; + runtimeBridge: RuntimeBridge; + release: () => void; +} + +export function createPiRuntimeServices(deps: PiRuntimeServicesDeps): PiRuntimeServices { + const credentialStore = new CredentialStore({ db: deps.db }); + const credentialResolver = new CredentialResolver(credentialStore); + const oauthLoginService = new OAuthLoginService({ + store: credentialStore, + emitEvent: deps.emitEvent, + refreshConnectorHealth: deps.refreshConnectorHealth, + logger: deps.logger, + }); + const runtimeBridge = new RuntimeBridge({ + db: deps.db, + emitEvent: deps.emitEvent, + logger: deps.logger, + credentialResolver, + }); + + return { + credentialStore, + credentialResolver, + oauthLoginService, + runtimeBridge, + release: () => { + runtimeBridge.approvalHook.cancelAll("AgentCompany shutting down."); + }, + }; +} diff --git a/src/main/suppress-warnings.ts b/src/main/suppress-warnings.ts new file mode 100644 index 00000000..0fb1700e --- /dev/null +++ b/src/main/suppress-warnings.ts @@ -0,0 +1,25 @@ +/** + * Silences the `node:sqlite` ExperimentalWarning emitted on first import. + * Must be imported before any module that touches `node:sqlite`, otherwise + * the warning has already fired. + * + * Targeted: only the SQLite experimental warning. All other warnings pass + * through unchanged. + */ + +const originalEmitWarning = process.emitWarning.bind(process); + +function isSqliteExperimentalWarning(warning: unknown): boolean { + if (typeof warning === "string") { + return /SQLite is an experimental feature/.test(warning); + } + if (warning instanceof Error) { + return warning.name === "ExperimentalWarning" && /SQLite/.test(warning.message); + } + return false; +} + +process.emitWarning = ((warning: unknown, ...rest: unknown[]) => { + if (isSqliteExperimentalWarning(warning)) return; + return (originalEmitWarning as (...args: unknown[]) => void)(warning, ...rest); +}) as typeof process.emitWarning; diff --git a/src/main/worker/log-redactor.ts b/src/main/worker/log-redactor.ts new file mode 100644 index 00000000..979d1b0c --- /dev/null +++ b/src/main/worker/log-redactor.ts @@ -0,0 +1,81 @@ +/** + * Defensive token redactor for worker process logs. + * + * Pi-mono's HTTP clients are not known to log credentials, but stack traces + * from upstream libraries (or future pi versions) could include header + * fragments containing `Authorization: Bearer ...` or `?access_token=...`. + * To prevent any such fragment from reaching the parent's stderr stream, + * `installLogRedactor()` wraps `console.error`, `console.warn`, `console.log`, + * `process.stdout.write`, and `process.stderr.write` so that every string + * argument is filtered through `redactTokens()` before transmission. + */ + +const PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [ + { pattern: /Bearer\s+[A-Za-z0-9._\-+/=]{8,}/g, replacement: "Bearer [REDACTED]" }, + { pattern: /(authorization\s*[:=]\s*)([^\s,;]{8,})/gi, replacement: "$1[REDACTED]" }, + { pattern: /(access_token=)([^&\s"']+)/gi, replacement: "$1[REDACTED]" }, + { pattern: /(refresh_token=)([^&\s"']+)/gi, replacement: "$1[REDACTED]" }, + { pattern: /(api[_-]?key\s*[:=]\s*['"]?)([A-Za-z0-9._-]{12,})/gi, replacement: "$1[REDACTED]" }, + { pattern: /sk-(?:ant-)?[A-Za-z0-9_-]{16,}/g, replacement: "sk-[REDACTED]" }, + { pattern: /ghp_[A-Za-z0-9]{16,}/g, replacement: "ghp_[REDACTED]" }, + { pattern: /gho_[A-Za-z0-9]{16,}/g, replacement: "gho_[REDACTED]" }, + { pattern: /AKIA[A-Z0-9]{16}/g, replacement: "AKIA[REDACTED]" }, +]; + +export function redactTokens(text: string): string { + let result = text; + for (const { pattern, replacement } of PATTERNS) { + result = result.replace(pattern, replacement); + } + return result; +} + +function redactArg(arg: unknown): unknown { + if (typeof arg === "string") return redactTokens(arg); + if (arg instanceof Error) { + const redacted = new Error(redactTokens(arg.message)); + redacted.name = arg.name; + if (arg.stack) redacted.stack = redactTokens(arg.stack); + return redacted; + } + return arg; +} + +interface InstallTarget { + console: { log: typeof console.log; warn: typeof console.warn; error: typeof console.error; info: typeof console.info }; + stdoutWrite: typeof process.stdout.write; + stderrWrite: typeof process.stderr.write; +} + +export function installLogRedactor(target?: Partial): () => void { + const cons = target?.console ?? console; + const origLog = cons.log.bind(cons); + const origWarn = cons.warn.bind(cons); + const origError = cons.error.bind(cons); + const origInfo = cons.info.bind(cons); + + cons.log = (...args: unknown[]) => origLog(...args.map(redactArg)); + cons.warn = (...args: unknown[]) => origWarn(...args.map(redactArg)); + cons.error = (...args: unknown[]) => origError(...args.map(redactArg)); + cons.info = (...args: unknown[]) => origInfo(...args.map(redactArg)); + + const stdoutWriteOrig = (target?.stdoutWrite ?? process.stdout.write).bind(process.stdout); + const stderrWriteOrig = (target?.stderrWrite ?? process.stderr.write).bind(process.stderr); + process.stdout.write = ((chunk: unknown, ...rest: unknown[]) => { + if (typeof chunk === "string") return stdoutWriteOrig(redactTokens(chunk), ...rest as never[]); + return stdoutWriteOrig(chunk as never, ...rest as never[]); + }) as typeof process.stdout.write; + process.stderr.write = ((chunk: unknown, ...rest: unknown[]) => { + if (typeof chunk === "string") return stderrWriteOrig(redactTokens(chunk), ...rest as never[]); + return stderrWriteOrig(chunk as never, ...rest as never[]); + }) as typeof process.stderr.write; + + return () => { + cons.log = origLog; + cons.warn = origWarn; + cons.error = origError; + cons.info = origInfo; + process.stdout.write = stdoutWriteOrig; + process.stderr.write = stderrWriteOrig; + }; +} diff --git a/src/renderer/components/execution/approval-inspector/SchedulePicker.tsx b/src/renderer/components/execution/approval-inspector/SchedulePicker.tsx index 3c9beb73..2f731779 100644 --- a/src/renderer/components/execution/approval-inspector/SchedulePicker.tsx +++ b/src/renderer/components/execution/approval-inspector/SchedulePicker.tsx @@ -25,7 +25,7 @@ export function SchedulePicker({ onScheduledAtChange: (value: string) => void onClearError: () => void }) { - const minDatetime = useMemo(computeMinDatetime, []) + const minDatetime = useMemo(() => computeMinDatetime(), []) return (

diff --git a/src/renderer/components/ui/AttentionRow.tsx b/src/renderer/components/ui/AttentionRow.tsx index 06a0c2b1..8f203568 100644 --- a/src/renderer/components/ui/AttentionRow.tsx +++ b/src/renderer/components/ui/AttentionRow.tsx @@ -1,6 +1,6 @@ import type { ElementType, ReactNode } from "react"; import { motion } from "framer-motion"; -import { ArrowRight } from "@phosphor-icons/react"; +import { ArrowRight } from "./icons"; const ATTENTION_STAGGER_MAX = 8; const ATTENTION_STAGGER_STEP = 0.04; diff --git a/src/renderer/components/ui/feedback.tsx b/src/renderer/components/ui/feedback.tsx index b31a70b5..ddeaf522 100644 --- a/src/renderer/components/ui/feedback.tsx +++ b/src/renderer/components/ui/feedback.tsx @@ -1,6 +1,6 @@ import { useEffect, type ReactNode } from "react"; import { AnimatePresence, motion } from "framer-motion"; -import { Trash, XCircle } from "@phosphor-icons/react"; +import { Trash, XCircle } from "./icons"; import { ActionButton } from "./primitives"; import { useFocusTrap } from "./hooks"; diff --git a/src/shared/contracts/schemas-agent-event.ts b/src/shared/contracts/schemas-agent-event.ts new file mode 100644 index 00000000..4dcf991f --- /dev/null +++ b/src/shared/contracts/schemas-agent-event.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; + +/** + * The renderer-bound `agent-event` push payload. + * + * The `event` payload is pi's AgentEvent (discriminated by `type`). We do not + * mirror pi's type system in Zod (it would couple the renderer to pi's + * internals); we validate the envelope and pass the event through opaquely. + */ +export const agentEventPushSchema = z.object({ + runId: z.string(), + agentId: z.string(), + companyId: z.string(), + taskId: z.string().nullable(), + emittedAt: z.string(), + event: z.object({ + type: z.string(), + }).passthrough(), +}); + +export type AgentEventPush = z.infer; diff --git a/src/shared/contracts/schemas-credentials.ts b/src/shared/contracts/schemas-credentials.ts new file mode 100644 index 00000000..daa90338 --- /dev/null +++ b/src/shared/contracts/schemas-credentials.ts @@ -0,0 +1,89 @@ +import { z } from "zod"; + +export const credentialKindSchema = z.enum([ + "oauth", + "api_key", + "aws", + "azure", + "gcp_adc", +]); + +export const credentialStatusSchema = z.enum([ + "active", + "expired", + "revoked", + "error", + "not_configured", +]); + +export const apiKeyValueSchema = z.object({ + source: z.enum(["literal", "env"]), + value: z.string().min(1), +}); + +export const azureCredentialSchema = z.object({ + apiKey: z.string().min(1), + baseUrl: z.string().url().optional().or(z.literal("")), + resourceName: z.string().optional().or(z.literal("")), + apiVersion: z.string().optional().or(z.literal("")), + deploymentMap: z.record(z.string(), z.string()).optional(), +}); + +export const awsCredentialSchema = z.object({ + authMode: z.enum(["profile", "iam", "bearer"]), + region: z.string().min(1).default("us-east-1"), + awsProfile: z.string().optional(), + accessKeyId: z.string().optional(), + secretAccessKey: z.string().optional(), + sessionToken: z.string().optional(), + bearerToken: z.string().optional(), + forceCache: z.boolean().optional(), + endpointUrl: z.string().optional(), + skipAuth: z.boolean().optional(), + forceHttp1: z.boolean().optional(), +}); + +export const gcpAdcCredentialSchema = z.object({ + project: z.string().min(1), + location: z.string().min(1), + credentialsPath: z.string().optional(), +}); + +export const credentialPayloadSchema = z.discriminatedUnion("kind", [ + z.object({ kind: z.literal("api_key"), value: apiKeyValueSchema }), + z.object({ kind: z.literal("azure"), value: azureCredentialSchema }), + z.object({ kind: z.literal("aws"), value: awsCredentialSchema }), + z.object({ kind: z.literal("gcp_adc"), value: gcpAdcCredentialSchema }), +]); + +export const saveCredentialInputSchema = z.object({ + connectorId: z.string().min(1), + payload: credentialPayloadSchema, +}); + +export const removeCredentialInputSchema = z.object({ + connectorId: z.string().min(1), +}); + +export const testCredentialInputSchema = z.object({ + connectorId: z.string().min(1), +}); + +export const credentialSummarySchema = z.object({ + connectorId: z.string(), + kind: credentialKindSchema, + displayHint: z.string(), + status: credentialStatusSchema, + expiresAt: z.string().nullable(), + lastRefreshedAt: z.string().nullable(), + lastError: z.string().nullable(), + updatedAt: z.string().nullable(), +}); + +export type CredentialSummary = z.infer; +export type CredentialPayload = z.infer; +export type SaveCredentialInput = z.infer; +export type AwsCredential = z.infer; +export type AzureCredential = z.infer; +export type GcpAdcCredential = z.infer; +export type ApiKeyValue = z.infer; diff --git a/src/shared/contracts/schemas-oauth.ts b/src/shared/contracts/schemas-oauth.ts new file mode 100644 index 00000000..60e51d2f --- /dev/null +++ b/src/shared/contracts/schemas-oauth.ts @@ -0,0 +1,60 @@ +import { z } from "zod"; + +export const oauthStartInputSchema = z.object({ + connectorId: z.string().min(1), +}); + +export const oauthCancelInputSchema = z.object({ + connectorId: z.string().min(1), +}); + +export const oauthPromptResponseSchema = z.object({ + connectorId: z.string().min(1), + requestId: z.string().min(1), + value: z.string(), +}); + +export const oauthManualCodeResponseSchema = z.object({ + connectorId: z.string().min(1), + requestId: z.string().min(1), + code: z.string().min(1), +}); + +export const oauthAuthInfoSchema = z.object({ + connectorId: z.string(), + url: z.string().url(), + instructions: z.string().optional(), +}); + +export const oauthProgressSchema = z.object({ + connectorId: z.string(), + message: z.string(), +}); + +export const oauthStatusSchema = z.object({ + connectorId: z.string(), + status: z.enum(["active", "cancelled", "error", "in_progress"]), + errorMessage: z.string().optional(), +}); + +export const oauthPromptRequestSchema = z.object({ + connectorId: z.string(), + requestId: z.string(), + message: z.string(), + placeholder: z.string().optional(), + allowEmpty: z.boolean().optional(), +}); + +export const oauthManualCodeRequestSchema = z.object({ + connectorId: z.string(), + requestId: z.string(), +}); + +export type OAuthStartInput = z.infer; +export type OAuthAuthInfoEvent = z.infer; +export type OAuthProgressEvent = z.infer; +export type OAuthStatusEvent = z.infer; +export type OAuthPromptRequestEvent = z.infer; +export type OAuthManualCodeRequestEvent = z.infer; +export type OAuthPromptResponseInput = z.infer; +export type OAuthManualCodeResponseInput = z.infer; diff --git a/src/shared/types/records-core.ts b/src/shared/types/records-core.ts index 172ee743..2ae3ca37 100644 --- a/src/shared/types/records-core.ts +++ b/src/shared/types/records-core.ts @@ -6,6 +6,7 @@ import type { WorkspaceState, DepartmentId, AgentStatus, + AgentRoleLevel, GoalStatus, ProjectStatus, TaskStatus, @@ -73,6 +74,8 @@ export interface AgentRecord { name: string; role: string; title: string; + /** Seniority enum used for leadership detection; free-text `role` remains UI-editable. */ + roleLevel: AgentRoleLevel; department: DepartmentId | null; status: AgentStatus; reportsTo: string | null; diff --git a/tests/integration/agent-model-switching.spec.ts b/tests/integration/agent-model-switching.spec.ts index df0c4b0e..7f8bc5ee 100644 --- a/tests/integration/agent-model-switching.spec.ts +++ b/tests/integration/agent-model-switching.spec.ts @@ -71,7 +71,7 @@ describe("agent model switching", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -98,7 +98,7 @@ describe("agent model switching", () => { title: "Frontend Engineer", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: "gpt-5.4", capabilities: "Build frontend features", @@ -147,7 +147,7 @@ describe("agent model switching", () => { title: "Frontend Engineer", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: "gpt-5.3-codex", capabilities: "Build frontend features", diff --git a/tests/integration/api-recursive-org-execution.spec.ts b/tests/integration/api-recursive-org-execution.spec.ts index 10a48c23..c90ed7ba 100644 --- a/tests/integration/api-recursive-org-execution.spec.ts +++ b/tests/integration/api-recursive-org-execution.spec.ts @@ -25,7 +25,7 @@ async function createHarness() { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -52,7 +52,7 @@ async function createHarness() { title: "Chief Executive Officer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -65,7 +65,7 @@ async function createHarness() { title: "Chief Technology Officer", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead engineering", @@ -81,7 +81,7 @@ async function createHarness() { priority: "high", status: "in_progress", }); - const run = db.createRun({ companyId, taskId, agentId: ctoId, workspaceId, connectorId: "codex_local" }); + const run = db.createRun({ companyId, taskId, agentId: ctoId, workspaceId, connectorId: "openai-codex" }); const token = createAgentToken(ctoId, companyId, run.id); const server = createApiServer(db, () => {}, () => null); const port = await server.start(); @@ -114,7 +114,7 @@ describe("recursive org execution", () => { role: "Engineer", title: "Frontend Engineer", department: "engineering", - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: harness.workspaceId, model: null, capabilities: "React, TypeScript, Tailwind, frontend delivery", @@ -176,7 +176,7 @@ describe("recursive org execution", () => { title: "Director of People", status: "active", reportsTo: harness.ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: harness.workspaceId, model: null, capabilities: "Coordinate staffing", @@ -197,7 +197,7 @@ describe("recursive org execution", () => { taskId: hrTaskId, agentId: hrId, workspaceId: harness.workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); const hrToken = createAgentToken(hrId, harness.companyId, hrRun.id); @@ -213,7 +213,7 @@ describe("recursive org execution", () => { role: "Backend Engineer", title: "Backend Engineer Golang", department: "engineering", - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: harness.workspaceId, model: null, capabilities: "Go, PostgreSQL, distributed systems", @@ -243,7 +243,7 @@ describe("recursive org execution", () => { role: "Backend Engineer", title: "Backend Engineer Golang", department: "engineering", - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: harness.workspaceId, model: null, capabilities: "Go, APIs", @@ -270,7 +270,7 @@ describe("recursive org execution", () => { title: "Chief Marketing Officer", status: "active", reportsTo: harness.ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: harness.workspaceId, model: null, capabilities: "Lead marketing", @@ -287,7 +287,7 @@ describe("recursive org execution", () => { title: "Misrouted Engineer", department: "engineering", reportsTo: cmoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: harness.workspaceId, model: null, capabilities: "Should not report outside the CTO branch", diff --git a/tests/integration/api-run-events.spec.ts b/tests/integration/api-run-events.spec.ts index a1bf8498..fc335d41 100644 --- a/tests/integration/api-run-events.spec.ts +++ b/tests/integration/api-run-events.spec.ts @@ -40,7 +40,7 @@ async function createHarness() { title: "Lead agent", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Operate the company", @@ -55,7 +55,7 @@ async function createHarness() { priority: "high", status: "todo", }); - const run = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "codex_local" }); + const run = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "openai-codex" }); const token = createAgentToken(agentId, companyId, run.id); const server = createApiServer(db, () => {}, () => null); const port = await server.start(); diff --git a/tests/integration/autonomous-chain/approval-decisions.spec.ts b/tests/integration/autonomous-chain/approval-decisions.spec.ts index a631269b..e42f2daf 100644 --- a/tests/integration/autonomous-chain/approval-decisions.spec.ts +++ b/tests/integration/autonomous-chain/approval-decisions.spec.ts @@ -65,7 +65,7 @@ describe("Step 4: Hire approval + decideApproval", () => { type: "hire_agent", payloadSummary: "Hire CTO", impactSummary: "Test", - payloadJson: JSON.stringify({ agentId: hireId, config: { connector: "codex_local" } }), + payloadJson: JSON.stringify({ agentId: hireId, config: { connector: "openai-codex" } }), }); const approval = shared.db.getApprovalRecord(approvalId); diff --git a/tests/integration/autonomous-chain/automation-messages.spec.ts b/tests/integration/autonomous-chain/automation-messages.spec.ts index 3aba169b..074d292c 100644 --- a/tests/integration/autonomous-chain/automation-messages.spec.ts +++ b/tests/integration/autonomous-chain/automation-messages.spec.ts @@ -91,13 +91,13 @@ describe("Agent Messages", () => { beforeEach(() => { agentAId = shared.db.saveAgent({ companyId: shared.companyId, name: "Alice", role: "Engineer", title: "Senior Engineer", - department: "engineering", connectorId: "claude_local", status: "active", + department: "engineering", connectorId: "claude-pro", status: "active", capabilities: "Backend development, API design, code review", budgetMonthlyUsd: 100, }); agentBId = shared.db.saveAgent({ companyId: shared.companyId, name: "Bob", role: "Designer", title: "UX Designer", - department: "design", connectorId: "claude_local", status: "active", + department: "design", connectorId: "claude-pro", status: "active", capabilities: "UI design, wireframing, user research", budgetMonthlyUsd: 100, }); @@ -242,13 +242,13 @@ describe("Auto Task Assignment", () => { beforeEach(() => { backendId = shared.db.saveAgent({ companyId: shared.companyId, name: "Backend Dev", role: "Backend Engineer", title: "Senior Backend", - department: "engineering", connectorId: "codex_local", status: "active", + department: "engineering", connectorId: "openai-codex", status: "active", capabilities: "API development, database design, Node.js, PostgreSQL, backend testing", budgetMonthlyUsd: 200, }); frontendId = shared.db.saveAgent({ companyId: shared.companyId, name: "Frontend Dev", role: "Frontend Engineer", title: "Senior Frontend", - department: "engineering", connectorId: "codex_local", status: "active", + department: "engineering", connectorId: "openai-codex", status: "active", capabilities: "React, TypeScript, CSS, Tailwind, responsive design, frontend testing, UI components", budgetMonthlyUsd: 200, }); @@ -290,8 +290,8 @@ describe("Auto Task Assignment", () => { it("returns null if all agents have exhausted their budget", () => { // Set positive budgets that are fully spent - shared.db.saveAgent({ id: backendId, companyId: shared.companyId, name: "Backend Dev", role: "Backend", title: "Backend", connectorId: "codex_local", status: "active", capabilities: "backend", budgetMonthlyUsd: 100 }); - shared.db.saveAgent({ id: frontendId, companyId: shared.companyId, name: "Frontend Dev", role: "Frontend", title: "Frontend", connectorId: "codex_local", status: "active", capabilities: "frontend", budgetMonthlyUsd: 100 }); + shared.db.saveAgent({ id: backendId, companyId: shared.companyId, name: "Backend Dev", role: "Backend", title: "Backend", connectorId: "openai-codex", status: "active", capabilities: "backend", budgetMonthlyUsd: 100 }); + shared.db.saveAgent({ id: frontendId, companyId: shared.companyId, name: "Frontend Dev", role: "Frontend", title: "Frontend", connectorId: "openai-codex", status: "active", capabilities: "frontend", budgetMonthlyUsd: 100 }); // Exhaust budgets by recording spending via direct SQL (since finishRun would need a real run) (shared.db as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } }).db.prepare("update agents set spent_monthly_usd = 100 where id = ?").run(backendId); (shared.db as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } }).db.prepare("update agents set spent_monthly_usd = 100 where id = ?").run(frontendId); diff --git a/tests/integration/autonomous-chain/crash-recovery.spec.ts b/tests/integration/autonomous-chain/crash-recovery.spec.ts index e9b9a9f5..2b702306 100644 --- a/tests/integration/autonomous-chain/crash-recovery.spec.ts +++ b/tests/integration/autonomous-chain/crash-recovery.spec.ts @@ -28,7 +28,7 @@ describe("Step 7: Worker crash recovery (database layer)", () => { taskId, agentId, workspaceId: shared.workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); shared.db.markRunStatus(run.id, "running", "Started"); @@ -66,7 +66,7 @@ describe("Step 7: Worker crash recovery (database layer)", () => { taskId, agentId, workspaceId: shared.workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); shared.db.markRunStatus(run.id, "running", "Started"); diff --git a/tests/integration/autonomous-chain/run-failure.spec.ts b/tests/integration/autonomous-chain/run-failure.spec.ts index f3ce65d3..7479943b 100644 --- a/tests/integration/autonomous-chain/run-failure.spec.ts +++ b/tests/integration/autonomous-chain/run-failure.spec.ts @@ -27,7 +27,7 @@ describe("Step 6: Run failure escalation (database layer)", () => { taskId, agentId, workspaceId: shared.workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); shared.db.markRunStatus(run.id, "running", "Started"); diff --git a/tests/integration/autonomous-chain/shared-setup.ts b/tests/integration/autonomous-chain/shared-setup.ts index 967427e0..72d1abc3 100644 --- a/tests/integration/autonomous-chain/shared-setup.ts +++ b/tests/integration/autonomous-chain/shared-setup.ts @@ -88,7 +88,7 @@ export function installSharedSetup(): SharedHandle { title: "Test", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: state.workspaceId, model: null, capabilities: "Test capabilities", diff --git a/tests/integration/connector-model-enforcement.spec.ts b/tests/integration/connector-model-enforcement.spec.ts index 7ab84312..2f914b0d 100644 --- a/tests/integration/connector-model-enforcement.spec.ts +++ b/tests/integration/connector-model-enforcement.spec.ts @@ -70,7 +70,7 @@ describe("connector model enforcement", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -97,7 +97,7 @@ describe("connector model enforcement", () => { title: "Chief Executive Officer", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: "gpt-5.4", capabilities: "Lead the company", @@ -113,7 +113,7 @@ describe("connector model enforcement", () => { status: "todo", }); - db.setDisabledModels("codex_local", [{ id: "gpt-5.4", label: "gpt-5.4" }]); + db.setDisabledModels("openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]); const { taskRunner } = createOrchestrationHarness(db); const result = taskRunner.queueTaskRun(taskId, "manual", "board"); @@ -134,7 +134,7 @@ describe("connector model enforcement", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -161,7 +161,7 @@ describe("connector model enforcement", () => { title: "Chief Executive Officer", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: "gpt-5.4", capabilities: "Lead the company", @@ -182,12 +182,12 @@ describe("connector model enforcement", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); const checkoutSuccess = db.checkoutTask(taskId, run.id); expect(checkoutSuccess).toBe(true); - db.setDisabledModels("codex_local", [{ id: "gpt-5.4", label: "gpt-5.4" }]); + db.setDisabledModels("openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]); const { taskRunner } = createOrchestrationHarness(db); taskRunner.resumeQueuedRuns(); diff --git a/tests/integration/cross-subsystem.spec.ts b/tests/integration/cross-subsystem.spec.ts index f5d22eae..79f4c97a 100644 --- a/tests/integration/cross-subsystem.spec.ts +++ b/tests/integration/cross-subsystem.spec.ts @@ -52,10 +52,10 @@ const mockLogger = { /** * Creates a full entity scaffold with a budget policy: - * company, workspace, connector (gemini_local), agent, goal, project, budget policy. + * company, workspace, connector (gemini-cli), agent, goal, project, budget policy. */ function scaffold(db: AppDatabase, budgetAmountCents = 500): ScaffoldResult { - const connectorId: ConnectorId = "gemini_local"; + const connectorId: ConnectorId = "gemini-cli"; db.saveConnector({ id: connectorId, @@ -228,7 +228,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { taskId, companyId: s.companyId, agentId: s.agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -387,7 +387,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { taskId, companyId: s.companyId, agentId: s.agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); diff --git a/tests/integration/database.spec.ts b/tests/integration/database.spec.ts index 511f5376..34ac0b92 100644 --- a/tests/integration/database.spec.ts +++ b/tests/integration/database.spec.ts @@ -1,7 +1,21 @@ import { mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; + +beforeAll(() => { + process.env.AGENTCOMPANY_ALLOW_INSECURE_VAULT = "true"; +}); + +vi.mock("electron", () => ({ + safeStorage: { + isEncryptionAvailable: () => false, + getSelectedStorageBackend: () => "basic_text", + encryptString: (value: string) => Buffer.from(value, "utf8"), + decryptString: (value: Buffer) => value.toString("utf8"), + }, +})); + import { AppDatabase } from "../../src/main/database"; const tempDirs: string[] = []; @@ -45,7 +59,7 @@ describe("AppDatabase", () => { title: "Lead agent", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Operate the company", @@ -89,7 +103,7 @@ describe("AppDatabase", () => { }); db.saveConnector({ - id: "codex_local", + id: "openai-codex", command: "codex", model: null, envBindingText: "OPENAI_API_KEY=vault-openai", @@ -105,7 +119,7 @@ describe("AppDatabase", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); await db.appendRunLog(run.id, "stdout", "hello from run\n"); @@ -190,7 +204,7 @@ describe("AppDatabase", () => { title: "", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: alphaWorkspaceId, model: null, capabilities: "Lead alpha", @@ -204,7 +218,7 @@ describe("AppDatabase", () => { title: "", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: betaWorkspaceId, model: null, capabilities: "Lead beta", diff --git a/tests/integration/document-run-provenance.spec.ts b/tests/integration/document-run-provenance.spec.ts index dcd83266..2f4208a5 100644 --- a/tests/integration/document-run-provenance.spec.ts +++ b/tests/integration/document-run-provenance.spec.ts @@ -26,7 +26,7 @@ async function createHarness() { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -53,7 +53,7 @@ async function createHarness() { title: "Research Lead", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Research and synthesis", @@ -86,7 +86,7 @@ async function createHarness() { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); const token = createAgentToken(agentId, companyId, run.id); const server = createApiServer(db, () => {}, () => null); diff --git a/tests/integration/onboarding-model-pool.spec.ts b/tests/integration/onboarding-model-pool.spec.ts index 4b37c628..9ea4edcb 100644 --- a/tests/integration/onboarding-model-pool.spec.ts +++ b/tests/integration/onboarding-model-pool.spec.ts @@ -6,6 +6,16 @@ import { AppDatabase } from "../../src/main/database"; import { createOnboardingService } from "../../src/main/onboarding-service"; import { buildRunPayload } from "../../src/main/orchestration/turn-context"; import { getConnectorDefinition } from "../../src/main/connectors"; +import { getModels } from "@mariozechner/pi-ai"; + +function listProviderModels(provider: string): Array<{ id: string; label: string }> { + try { + const models = getModels(provider as never); + return models.map((model) => ({ id: model.id, label: model.name ?? model.id })); + } catch { + return []; + } +} const tempDirs: string[] = []; @@ -80,22 +90,22 @@ describe("onboarding model pool integration", () => { const db = new AppDatabase(profileDir); db.init(); - const codexDefinition = getConnectorDefinition("codex_local"); - const enabledModel = codexDefinition.module.models?.[0]?.id; - const disabledModel = codexDefinition.module.models?.[1]?.id; + const codexDefinition = getConnectorDefinition("openai-codex"); + const enabledModel = listProviderModels(codexDefinition.provider)[0]?.id; + const disabledModel = listProviderModels(codexDefinition.provider)[1]?.id; if (!enabledModel || !disabledModel) { - throw new Error("codex_local requires at least two models for this integration test."); + throw new Error("openai-codex requires at least two models for this integration test."); } db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", lastError: null, }); - db.setDisabledModels("codex_local", [{ id: disabledModel, label: disabledModel }]); + db.setDisabledModels("openai-codex", [{ id: disabledModel, label: disabledModel }]); const summary = createBootstrapHarness(db).bootstrapOnboarding({ companyName: "Model Governance Co", @@ -106,7 +116,7 @@ describe("onboarding model pool integration", () => { executionMode: "ai_ceo", goalTitle: "Launch runtime-safe company", goalDescription: "Ensure onboarding connector models flow into runtime decisions.", - connectorId: "codex_local", + connectorId: "openai-codex", connectorModel: enabledModel, }); @@ -115,9 +125,9 @@ describe("onboarding model pool integration", () => { } const agent = db.getAgent(summary.leadAgentId); - expect(agent.connectorId).toBe("codex_local"); + expect(agent.connectorId).toBe("openai-codex"); expect(agent.model).toBe(enabledModel); - expect(db.getDisabledModels("codex_local")).toEqual([{ id: disabledModel, label: disabledModel }]); + expect(db.getDisabledModels("openai-codex")).toEqual([{ id: disabledModel, label: disabledModel }]); expect(db.getCurrentCompanyId()).toBe(summary.companyId); const payload = buildKickoffPayload(db, summary); @@ -128,7 +138,7 @@ describe("onboarding model pool integration", () => { expect(systemPrompt).toContain(enabledModel); const codexConnectorLine = systemPrompt .split("\n") - .find((line) => line.includes("**Codex**")); + .find((line) => line.includes("ChatGPT Plus/Pro (Codex)")); expect(codexConnectorLine).toBeDefined(); expect(codexConnectorLine).not.toContain(`, ${disabledModel},`); expect(codexConnectorLine).not.toContain(` ${disabledModel}.`); @@ -136,46 +146,53 @@ describe("onboarding model pool integration", () => { db.close(); }); - it("uses compat connector csv models minus disabled entries in runtime prompt context", async () => { + it("excludes disabled pi-ai models from anthropic connector runtime prompt context", async () => { const profileDir = await createTempDir("agentcompany-onboarding-compat-"); const workspaceDir = await createTempDir("agentcompany-onboarding-compat-workspace-"); const db = new AppDatabase(profileDir); db.init(); - db.saveConnector({ - id: "claude_compatible", - command: "claude", - model: "model-alpha,model-beta,model-gamma", - envBindingText: "", - notes: "Custom provider", - }); + const anthropicDefinition = getConnectorDefinition("anthropic"); + const anthropicModels = listProviderModels(anthropicDefinition.provider); + const enabledModel = anthropicModels[0]?.id; + const disabledModel = anthropicModels[1]?.id; + if (!enabledModel || !disabledModel) { + throw new Error("anthropic provider must expose at least two models from pi-ai."); + } + db.updateConnectorHealth({ - id: "claude_compatible", + id: "anthropic", status: "ready", authState: "authenticated", version: "test", lastError: null, }); - db.setDisabledModels("claude_compatible", [{ id: "model-beta", label: "model-beta" }]); + db.setDisabledModels("anthropic", [{ id: disabledModel, label: disabledModel }]); const summary = createBootstrapHarness(db).bootstrapOnboarding({ - companyName: "Compat Governance Co", - companyDescription: "Tests compat model governance", + companyName: "Anthropic Governance Co", + companyDescription: "Tests anthropic model governance", workspacePath: workspaceDir, autoApproveHires: true, reviewDeliverables: false, executionMode: "ai_ceo", - goalTitle: "Launch compat company", - goalDescription: "Ensure compat model pools survive onboarding and runtime prompt assembly.", - connectorId: "claude_compatible", - connectorModel: "model-alpha", + goalTitle: "Launch anthropic company", + goalDescription: "Ensure anthropic model pools survive onboarding and runtime prompt assembly.", + connectorId: "anthropic", + connectorModel: enabledModel, }); const payload = buildKickoffPayload(db, summary); expect(payload.systemPrompt).not.toBeNull(); const systemPrompt = payload.systemPrompt ?? ""; - expect(systemPrompt).toContain("Enabled models: model-alpha, model-gamma."); - expect(systemPrompt).not.toContain("model-beta"); + expect(systemPrompt).toContain("Enabled models:"); + expect(systemPrompt).toContain(enabledModel); + const anthropicLine = systemPrompt + .split("\n") + .find((line) => line.includes(anthropicDefinition.label)); + expect(anthropicLine).toBeDefined(); + expect(anthropicLine).not.toContain(`, ${disabledModel},`); + expect(anthropicLine).not.toContain(` ${disabledModel}.`); db.close(); }); @@ -187,7 +204,7 @@ describe("onboarding model pool integration", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -203,7 +220,7 @@ describe("onboarding model pool integration", () => { executionMode: "ai_ceo", goalTitle: "Launch default-mode company", goalDescription: "Leave the CEO model unset so runtime falls back to connector defaults.", - connectorId: "codex_local", + connectorId: "openai-codex", connectorModel: "", }); @@ -215,7 +232,7 @@ describe("onboarding model pool integration", () => { expect(agent.model).toBeNull(); const payload = buildKickoffPayload(db, summary); - const defaultModel = getConnectorDefinition("codex_local").defaultModel; + const defaultModel = getConnectorDefinition("openai-codex").defaultModel; expect(payload.model).toBe(defaultModel); db.close(); @@ -227,22 +244,22 @@ describe("onboarding model pool integration", () => { const db = new AppDatabase(profileDir); db.init(); - const codexDefinition = getConnectorDefinition("codex_local"); + const codexDefinition = getConnectorDefinition("openai-codex"); const defaultModel = codexDefinition.defaultModel; - const fallbackModel = codexDefinition.module.models?.find((model) => model.id !== defaultModel)?.id; + const fallbackModel = listProviderModels(codexDefinition.provider).find((model) => model.id !== defaultModel)?.id; if (!defaultModel || !fallbackModel) { - throw new Error("codex_local requires a default and fallback model for reroute testing."); + throw new Error("openai-codex requires a default and fallback model for reroute testing."); } db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", lastError: null, }); - db.setDisabledModels("codex_local", [{ id: defaultModel, label: defaultModel }]); + db.setDisabledModels("openai-codex", [{ id: defaultModel, label: defaultModel }]); const summary = createBootstrapHarness(db).bootstrapOnboarding({ companyName: "Default Reroute Co", @@ -253,7 +270,7 @@ describe("onboarding model pool integration", () => { executionMode: "ai_ceo", goalTitle: "Launch rerouted company", goalDescription: "Ensure runtime picks the first enabled model when connector default is disabled.", - connectorId: "codex_local", + connectorId: "openai-codex", connectorModel: "", }); @@ -269,21 +286,21 @@ describe("onboarding model pool integration", () => { const db = new AppDatabase(profileDir); db.init(); - const codexDefinition = getConnectorDefinition("codex_local"); - const disabledModel = codexDefinition.module.models?.[0]?.id; + const codexDefinition = getConnectorDefinition("openai-codex"); + const disabledModel = listProviderModels(codexDefinition.provider)[0]?.id; if (!disabledModel) { - throw new Error("codex_local requires a model to validate onboarding rejection."); + throw new Error("openai-codex requires a model to validate onboarding rejection."); } db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", lastError: null, }); - db.setDisabledModels("codex_local", [{ id: disabledModel, label: disabledModel }]); + db.setDisabledModels("openai-codex", [{ id: disabledModel, label: disabledModel }]); expect(() => createBootstrapHarness(db).bootstrapOnboarding({ companyName: "Rejected Model Co", @@ -294,9 +311,9 @@ describe("onboarding model pool integration", () => { executionMode: "ai_ceo", goalTitle: "Reject disabled model bootstrap", goalDescription: "Invalid explicit models should fail before the CEO is created.", - connectorId: "codex_local", + connectorId: "openai-codex", connectorModel: disabledModel, - })).toThrow(`Codex does not allow model "${disabledModel}".`); + })).toThrow(`${codexDefinition.label} does not allow model "${disabledModel}".`); db.close(); }); diff --git a/tests/integration/pi-mono-migrations.spec.ts b/tests/integration/pi-mono-migrations.spec.ts new file mode 100644 index 00000000..bb40ae73 --- /dev/null +++ b/tests/integration/pi-mono-migrations.spec.ts @@ -0,0 +1,153 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeAll, describe, expect, it } from "vitest"; +import { AppDatabase } from "../../src/main/database"; + +beforeAll(() => { + // SecretVault gated for test contexts. Used by the credentials path + // touched by migration 28's downstream consumers. + process.env.AGENTCOMPANY_ALLOW_INSECURE_VAULT = "true"; +}); + +const tempDirs: string[] = []; + +async function freshProfile(prefix: string) { + const dir = await mkdtemp(join(tmpdir(), prefix)); + tempDirs.push(dir); + return dir; +} + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +/** + * Migration 27 (`session_entries`) and 28 (`credentials`) ship in the rebuild. + * These tests verify both migrations: + * - run cleanly on a fresh SQLite profile; + * - are idempotent on subsequent `init()` calls (existing-user upgrade); + * - leave their tables queryable. + */ +describe("pi-mono migrations 27 + 28", () => { + it("creates session_entries on first init and accepts inserts", async () => { + const dir = await freshProfile("agentcompany-migration-27-fresh-"); + const db = new AppDatabase(dir); + db.init(); + + db.sessionEntries.append({ + taskSessionId: "test-session", + kind: "message", + role: "user", + payloadJson: JSON.stringify({ role: "user", content: "hello" }), + }); + const rows = db.sessionEntries.listActive("test-session"); + expect(rows).toHaveLength(1); + expect(rows[0]?.kind).toBe("message"); + db.close(); + }); + + it("creates credentials on first init and accepts encrypted inserts", async () => { + const dir = await freshProfile("agentcompany-migration-28-fresh-"); + const db = new AppDatabase(dir); + db.init(); + + const cipher = db.vault.encrypt(JSON.stringify({ source: "literal", value: "sk-test-1234567890" })); + db.credentialsTable.upsert({ + connectorId: "test-anthropic", + kind: "api_key", + backend: cipher.backend, + ciphertext: cipher.payload, + displayHint: "sk-test-...7890", + status: "active", + expiresAt: null, + lastRefreshedAt: null, + lastError: null, + }); + + const rows = db.credentialsTable.list(); + expect(rows.some((row) => row.connectorId === "test-anthropic")).toBe(true); + db.close(); + }); + + it("re-running init() on an existing profile does not re-apply migrations or duplicate rows", async () => { + const dir = await freshProfile("agentcompany-migration-idempotent-"); + const dbA = new AppDatabase(dir); + dbA.init(); + dbA.sessionEntries.append({ + taskSessionId: "session-x", + kind: "message", + role: "user", + payloadJson: JSON.stringify({ role: "user", content: "first" }), + }); + const cipher = dbA.vault.encrypt(JSON.stringify({ source: "literal", value: "sk-keep-me" })); + dbA.credentialsTable.upsert({ + connectorId: "test-keep", + kind: "api_key", + backend: cipher.backend, + ciphertext: cipher.payload, + displayHint: "sk-keep-...e", + status: "active", + expiresAt: null, + lastRefreshedAt: null, + lastError: null, + }); + dbA.close(); + + const dbB = new AppDatabase(dir); + dbB.init(); + expect(dbB.sessionEntries.listActive("session-x")).toHaveLength(1); + expect(dbB.credentialsTable.list().filter((row) => row.connectorId === "test-keep")).toHaveLength(1); + dbB.close(); + }); + + it("session_entries respects the unique (task_session_id, sequence) index across replace cycles", async () => { + const dir = await freshProfile("agentcompany-migration-replace-"); + const db = new AppDatabase(dir); + db.init(); + + for (let i = 0; i < 3; i += 1) { + db.sessionEntries.append({ + taskSessionId: "replace-test", + kind: "message", + role: "user", + payloadJson: JSON.stringify({ role: "user", content: `t${i}` }), + }); + } + db.sessionEntries.replace({ + taskSessionId: "replace-test", + entries: [ + { kind: "compaction", role: null, payloadJson: JSON.stringify({ summary: "summarised" }) }, + { kind: "message", role: "user", payloadJson: JSON.stringify({ role: "user", content: "post" }) }, + ], + }); + + const active = db.sessionEntries.listActive("replace-test"); + expect(active).toHaveLength(2); + expect(active[0]?.kind).toBe("compaction"); + expect(active[1]?.kind).toBe("message"); + + const all = db.sessionEntries.listAll("replace-test"); + expect(all.length).toBe(5); + expect(all.filter((row) => row.isSuperseded).length).toBe(3); + db.close(); + }); + + it("seedConnectors prunes legacy connector rows and inserts the full pi-mono + Fireworks catalog", async () => { + const dir = await freshProfile("agentcompany-migration-seed-"); + const db = new AppDatabase(dir); + db.init(); + + const rows = db.listSnapshot().connectors.map((entry) => entry.id); + expect(rows.length).toBe(26); + expect(rows).toContain("claude-pro"); + expect(rows).toContain("amazon-bedrock"); + expect(rows).toContain("google-vertex"); + expect(rows).toContain("fireworks"); + expect(rows).toContain("fireworks-firepass"); + expect(rows.find((id) => (id as string) === "claude_local")).toBeUndefined(); + expect(rows.find((id) => (id as string) === "codex_local")).toBeUndefined(); + expect(rows.find((id) => (id as string) === "claude_compatible")).toBeUndefined(); + db.close(); + }); +}); diff --git a/tests/integration/process-recovery.spec.ts b/tests/integration/process-recovery.spec.ts index 2eeebfd5..7133360b 100644 --- a/tests/integration/process-recovery.spec.ts +++ b/tests/integration/process-recovery.spec.ts @@ -51,7 +51,7 @@ interface ScaffoldResult { /** Creates a full entity scaffold: company, workspace, connector, agent, goal, project. */ function scaffold(db: AppDatabase): ScaffoldResult { - const connectorId: ConnectorId = "gemini_local"; + const connectorId: ConnectorId = "gemini-cli"; // Connector db.saveConnector({ @@ -445,7 +445,7 @@ describe("Process Recovery -- Context Preservation (D-02)", () => { taskId, companyId: s.companyId, agentId: s.agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -466,7 +466,7 @@ describe("Process Recovery -- Context Preservation (D-02)", () => { const rotationResult = sessionService.evaluateRotation( session!, - "gemini_local", + "gemini-cli", { maxRuns: 1 }, ); expect(rotationResult.shouldRotate).toBe(true); diff --git a/tests/integration/skill-aware-assignment.spec.ts b/tests/integration/skill-aware-assignment.spec.ts index d7b0913a..f82b4b67 100644 --- a/tests/integration/skill-aware-assignment.spec.ts +++ b/tests/integration/skill-aware-assignment.spec.ts @@ -25,7 +25,7 @@ describe("skill-aware capability registry", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -53,7 +53,7 @@ describe("skill-aware capability registry", () => { title: "Engineer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "General software delivery", @@ -66,7 +66,7 @@ describe("skill-aware capability registry", () => { title: "Frontend Specialist", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "General software delivery", @@ -116,7 +116,7 @@ describe("skill-aware capability registry", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -143,7 +143,7 @@ describe("skill-aware capability registry", () => { title: "Chief Executive Officer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -193,7 +193,7 @@ describe("skill-aware capability registry", () => { recentComments: [], socialAccounts: [], allConnectors: snapshot.connectors - .filter((connector) => connector.id === "codex_local") + .filter((connector) => connector.id === "openai-codex") .map((connector) => ({ id: connector.id, label: connector.label, status: connector.status, enabledModels: ["gpt-5.4"] })), companySkillCatalog: db.listCompanySkills(companyId).map((skill) => ({ name: skill.name, diff --git a/tests/integration/thread-wait-heartbeat.spec.ts b/tests/integration/thread-wait-heartbeat.spec.ts index 2567eef1..4bb44213 100644 --- a/tests/integration/thread-wait-heartbeat.spec.ts +++ b/tests/integration/thread-wait-heartbeat.spec.ts @@ -29,7 +29,7 @@ function createNoopLogger() { function createScaffold(db: AppDatabase) { db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -57,7 +57,7 @@ function createScaffold(db: AppDatabase) { title: "Chief Executive Officer", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -70,7 +70,7 @@ function createScaffold(db: AppDatabase) { title: "Lead Engineer", status: "idle", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Ship features", @@ -140,7 +140,7 @@ describe("thread wait + heartbeat integration", () => { title: "Chief Marketing Officer", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: db.getTask(taskId).workspaceId ?? null, model: null, capabilities: "Drive marketing work", diff --git a/tests/unit/agent-budget-policy-sync.test.ts b/tests/unit/agent-budget-policy-sync.test.ts index b4c36e21..23898bbc 100644 --- a/tests/unit/agent-budget-policy-sync.test.ts +++ b/tests/unit/agent-budget-policy-sync.test.ts @@ -28,7 +28,7 @@ describe("agent budget policy synchronization", () => { status: "active", }); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -49,7 +49,7 @@ describe("agent budget policy synchronization", () => { title: "Backend Engineer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Build backend services", @@ -78,7 +78,7 @@ describe("agent budget policy synchronization", () => { title: "Backend Engineer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Build backend services", diff --git a/tests/unit/agent-hire-routing.test.ts b/tests/unit/agent-hire-routing.test.ts index 2a615dd1..dd6b44b6 100644 --- a/tests/unit/agent-hire-routing.test.ts +++ b/tests/unit/agent-hire-routing.test.ts @@ -29,7 +29,7 @@ describe("hire routing guards", () => { status: "active", }); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -50,7 +50,7 @@ describe("hire routing guards", () => { title: "Chief Executive Officer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -64,7 +64,7 @@ describe("hire routing guards", () => { title: "Head of HR", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Run hiring operations", @@ -78,7 +78,7 @@ describe("hire routing guards", () => { title: "Chief Technology Officer", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead engineering", diff --git a/tests/unit/agent-permissions.test.ts b/tests/unit/agent-permissions.test.ts index c853af93..59c9976b 100644 --- a/tests/unit/agent-permissions.test.ts +++ b/tests/unit/agent-permissions.test.ts @@ -51,7 +51,7 @@ describe("canManageOrganization", () => { title: "Chief Executive Officer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -64,7 +64,7 @@ describe("canManageOrganization", () => { title: "Chief Technology Officer", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead engineering", @@ -78,7 +78,7 @@ describe("canManageOrganization", () => { title: "Software Engineer", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Build features", @@ -120,7 +120,7 @@ describe("canManageOrganization", () => { title: "Chief Executive Officer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -133,7 +133,7 @@ describe("canManageOrganization", () => { title: "Operations Specialist", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Coordinate operations", @@ -174,7 +174,7 @@ describe("canManageOrganization", () => { title: "Chief Executive Officer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -187,7 +187,7 @@ describe("canManageOrganization", () => { title: "Chief Technology Officer", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead engineering", @@ -201,7 +201,7 @@ describe("canManageOrganization", () => { title: "Chief Marketing Officer", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead marketing", @@ -215,7 +215,7 @@ describe("canManageOrganization", () => { title: "Frontend Engineer", status: "active", reportsTo: ctoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Build frontend features", diff --git a/tests/unit/agent-schema-validation.test.ts b/tests/unit/agent-schema-validation.test.ts index 08ff2bea..f2516e41 100644 --- a/tests/unit/agent-schema-validation.test.ts +++ b/tests/unit/agent-schema-validation.test.ts @@ -10,7 +10,7 @@ const baseHireRequest = { title: "Backend Engineer", department: "engineering" as const, reportsTo: null, - connectorId: "codex_local" as const, + connectorId: "openai-codex" as const, workspaceId: null, model: null, metadataJson: "{}", diff --git a/tests/unit/approval-hire-routing.test.ts b/tests/unit/approval-hire-routing.test.ts index c8c7fda9..f7d7d19a 100644 --- a/tests/unit/approval-hire-routing.test.ts +++ b/tests/unit/approval-hire-routing.test.ts @@ -24,7 +24,7 @@ describe("hire approval routing", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -51,7 +51,7 @@ describe("hire approval routing", () => { title: "Chief Executive Officer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -64,7 +64,7 @@ describe("hire approval routing", () => { title: "Chief Technology Officer", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead engineering", @@ -80,7 +80,7 @@ describe("hire approval routing", () => { title: "Frontend Engineer", department: "engineering", reportsTo: ctoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, metadataJson: "{}", diff --git a/tests/unit/approval-hook.test.ts b/tests/unit/approval-hook.test.ts index d3968cd1..e4262750 100644 --- a/tests/unit/approval-hook.test.ts +++ b/tests/unit/approval-hook.test.ts @@ -49,7 +49,7 @@ describe("HumanApprovalHook", () => { title: "Eng", status: "running", reportsTo: null, - connectorId: "claude_local", + connectorId: "claude-pro", workspaceId, model: null, capabilities: "Build", diff --git a/tests/unit/approval-review-feedback-writeback.test.ts b/tests/unit/approval-review-feedback-writeback.test.ts index 4ba6972e..649f592e 100644 --- a/tests/unit/approval-review-feedback-writeback.test.ts +++ b/tests/unit/approval-review-feedback-writeback.test.ts @@ -24,7 +24,7 @@ describe("deliverable review feedback writeback", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -51,7 +51,7 @@ describe("deliverable review feedback writeback", () => { title: "Research Lead", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Research and synthesis", diff --git a/tests/unit/atomic-checkout.test.ts b/tests/unit/atomic-checkout.test.ts index 2bfa7ef0..62fffd1d 100644 --- a/tests/unit/atomic-checkout.test.ts +++ b/tests/unit/atomic-checkout.test.ts @@ -37,7 +37,7 @@ function createTestCompany(db: AppDatabase): string { function createTestConnector(db: AppDatabase): void { db.saveConnector({ - id: "codex_local", + id: "openai-codex", command: "codex", model: "gpt-5", envBindingText: "", @@ -64,7 +64,7 @@ function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string title: name, status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Executes tasks", @@ -115,7 +115,7 @@ describe("CAS checkout", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); const result = db.checkoutTask(taskId, runId); @@ -137,14 +137,14 @@ describe("CAS checkout", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); const { id: runId2 } = db.createRun({ companyId, taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); expect(db.checkoutTask(taskId, runId1)).toBe(true); @@ -166,7 +166,7 @@ describe("CAS checkout", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); expect(db.checkoutTask(taskId, runId)).toBe(false); @@ -184,7 +184,7 @@ describe("CAS checkout", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); db.checkoutTask(taskId, runId); @@ -222,7 +222,7 @@ describe("CAS checkout", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); db.checkoutTask(taskId, runId); @@ -251,7 +251,7 @@ describe("CAS checkout", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); db.checkoutTask(taskId, runId); @@ -291,7 +291,7 @@ describe("CAS checkout", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); db.checkoutTask(taskId, runId); diff --git a/tests/unit/capability-registry.test.ts b/tests/unit/capability-registry.test.ts index bac01225..8b4b5f4c 100644 --- a/tests/unit/capability-registry.test.ts +++ b/tests/unit/capability-registry.test.ts @@ -11,7 +11,7 @@ describe("buildCapabilityRegistrySection", () => { { name: "Primary", localPath: "/tmp/workspace" }, ], allConnectors: [ - { id: "codex_local", label: "Codex", status: "ready", enabledModels: ["gpt-5.4"] }, + { id: "openai-codex", label: "Codex", status: "ready", enabledModels: ["gpt-5.4"] }, ], companyDocuments: [ { @@ -38,7 +38,7 @@ describe("buildCapabilityRegistrySection", () => { expect(section).toContain("# Company Capability Registry"); expect(section).toContain("React Standards (`react-standards`)"); expect(section).toContain("Primary: /tmp/workspace"); - expect(section).toContain("Codex (`codex_local`) — models: gpt-5.4"); + expect(section).toContain("Codex (`openai-codex`) — models: gpt-5.4"); expect(section).toContain("architecture_decision"); }); }); diff --git a/tests/unit/command-paths.test.ts b/tests/unit/command-paths.test.ts index 6d710842..73a294bf 100644 --- a/tests/unit/command-paths.test.ts +++ b/tests/unit/command-paths.test.ts @@ -1,15 +1,10 @@ import { describe, expect, it } from "vitest"; -import { buildWindowsSpawnTarget, normalizeCommandValue } from "@agentcompany/adapter-utils/server-utils"; -import { commandLooksLike } from "@agentcompany/adapter-utils/test-utils"; +import { normalizeCommandValue } from "@main/connector-helpers"; describe("command path normalization", () => { - it("removes surrounding quotes from absolute executable paths", () => { - expect(normalizeCommandValue('"C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe"')).toBe( - "C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe", - ); - expect(normalizeCommandValue("'C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe'")).toBe( - "C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe", - ); + it("trims surrounding whitespace from command values", () => { + expect(normalizeCommandValue(" claude ")).toBe("claude"); + expect(normalizeCommandValue("\tcodex\n")).toBe("codex"); }); it("keeps bare commands unchanged", () => { @@ -17,31 +12,8 @@ describe("command path normalization", () => { expect(normalizeCommandValue("codex")).toBe("codex"); }); - it("matches quoted commands against connector executables", () => { - expect(commandLooksLike('"C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe"', "claude")).toBe(true); - expect(commandLooksLike("'C:\\Users\\First Last\\AppData\\Local\\bin\\codex.exe'", "codex")).toBe(true); - }); - - it("spawns direct exe paths on Windows instead of wrapping them through cmd.exe", () => { - const target = buildWindowsSpawnTarget( - "C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe", - ["--version"], - "cmd.exe", - ); - expect(target).toEqual({ - command: "C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe", - args: ["--version"], - }); - }); - - it("still wraps shell scripts through cmd.exe on Windows", () => { - const target = buildWindowsSpawnTarget( - "C:\\Tools\\agentcompany\\launch.cmd", - ["--version"], - "cmd.exe", - ); - expect(target.command).toBe("cmd.exe"); - expect(target.args[0]).toBe("/d"); - expect(target.args.at(-1)).toContain("launch.cmd"); + it("normalizes null and undefined to empty string", () => { + expect(normalizeCommandValue(null)).toBe(""); + expect(normalizeCommandValue(undefined)).toBe(""); }); }); diff --git a/tests/unit/company-mode-switch.test.ts b/tests/unit/company-mode-switch.test.ts index a6be8c23..5f957836 100644 --- a/tests/unit/company-mode-switch.test.ts +++ b/tests/unit/company-mode-switch.test.ts @@ -33,7 +33,7 @@ describe("company mode switching", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -63,7 +63,7 @@ describe("company mode switching", () => { title: "Chief Executive Officer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -76,7 +76,7 @@ describe("company mode switching", () => { title: "Lead Engineer", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Ship features", diff --git a/tests/unit/connector-matching.test.ts b/tests/unit/connector-matching.test.ts index aa247130..6bed1932 100644 --- a/tests/unit/connector-matching.test.ts +++ b/tests/unit/connector-matching.test.ts @@ -3,38 +3,37 @@ import { inferTaskType, getConnectorStrengths, TASK_TYPE_PREFERENCE, - CONNECTOR_STRENGTHS as _CONNECTOR_STRENGTHS, ROLE_DEPARTMENT_HINTS as _ROLE_DEPARTMENT_HINTS, } from "../../src/main/connector-matching"; describe("connector-matching", () => { describe("TASK_TYPE_PREFERENCE", () => { - it("maps code tasks to claude_local first", () => { - expect(TASK_TYPE_PREFERENCE.code[0]).toBe("claude_local"); + it("maps code tasks to claude-pro first", () => { + expect(TASK_TYPE_PREFERENCE.code[0]).toBe("claude-pro"); }); - it("maps research tasks to gemini_local first", () => { - expect(TASK_TYPE_PREFERENCE.research[0]).toBe("gemini_local"); + it("maps research tasks to google first", () => { + expect(TASK_TYPE_PREFERENCE.research[0]).toBe("google"); }); - it("maps content tasks to claude_local first", () => { - expect(TASK_TYPE_PREFERENCE.content[0]).toBe("claude_local"); + it("maps content tasks to claude-pro first", () => { + expect(TASK_TYPE_PREFERENCE.content[0]).toBe("claude-pro"); }); - it("maps management tasks to claude_local first", () => { - expect(TASK_TYPE_PREFERENCE.management[0]).toBe("claude_local"); + it("maps management tasks to claude-pro first", () => { + expect(TASK_TYPE_PREFERENCE.management[0]).toBe("claude-pro"); }); - it("maps general tasks to claude_local first", () => { - expect(TASK_TYPE_PREFERENCE.general[0]).toBe("claude_local"); + it("maps general tasks to claude-pro first", () => { + expect(TASK_TYPE_PREFERENCE.general[0]).toBe("claude-pro"); }); it("includes all three core connectors for every task type", () => { for (const key of Object.keys(TASK_TYPE_PREFERENCE)) { const prefs = TASK_TYPE_PREFERENCE[key as keyof typeof TASK_TYPE_PREFERENCE]; - expect(prefs).toContain("claude_local"); - expect(prefs).toContain("codex_local"); - expect(prefs).toContain("gemini_local"); + expect(prefs).toContain("claude-pro"); + expect(prefs).toContain("openai-codex"); + expect(prefs).toContain("gemini-cli"); } }); }); @@ -102,22 +101,22 @@ describe("connector-matching", () => { }); describe("getConnectorStrengths", () => { - it("returns non-empty string for claude_local", () => { - const result = getConnectorStrengths("claude_local"); + it("returns non-empty string for claude-pro", () => { + const result = getConnectorStrengths("claude-pro"); expect(result).toBeTruthy(); expect(typeof result).toBe("string"); expect(result.length).toBeGreaterThan(10); }); - it("returns non-empty string for codex_local", () => { - const result = getConnectorStrengths("codex_local"); + it("returns non-empty string for openai-codex", () => { + const result = getConnectorStrengths("openai-codex"); expect(result).toBeTruthy(); expect(typeof result).toBe("string"); expect(result.length).toBeGreaterThan(10); }); - it("returns non-empty string for gemini_local", () => { - const result = getConnectorStrengths("gemini_local"); + it("returns non-empty string for gemini-cli", () => { + const result = getConnectorStrengths("gemini-cli"); expect(result).toBeTruthy(); expect(typeof result).toBe("string"); expect(result.length).toBeGreaterThan(10); diff --git a/tests/unit/connector-model-policy.test.ts b/tests/unit/connector-model-policy.test.ts index a428c1b2..af7d5a4c 100644 --- a/tests/unit/connector-model-policy.test.ts +++ b/tests/unit/connector-model-policy.test.ts @@ -8,72 +8,85 @@ function createDeps(overrides: { } = {}) { return { getConnector: () => ({ - id: "claude_compatible" as const, - label: "Claude Compatible", - model: overrides.connectorModel ?? "model-alpha,model-beta,model-gamma", + id: "anthropic" as const, + label: "Anthropic", + model: overrides.connectorModel ?? null, }), getDisabledModels: () => overrides.disabledModels ?? [], }; } describe("connector model policy", () => { - it("derives enabled compat models from connector config minus disabled entries", () => { - const policy = getConnectorModelPolicy( - createDeps({ disabledModels: [{ id: "model-beta", label: "model-beta" }] }), - "claude_compatible", + it("excludes disabled models from the enabled set", () => { + const policy = getConnectorModelPolicy(createDeps(), "anthropic"); + const allIds = policy.enabledModels.map((model) => model.id); + if (allIds.length === 0) { + throw new Error("anthropic provider should expose at least one model from pi-ai."); + } + + const firstModel = allIds[0]; + const filteredPolicy = getConnectorModelPolicy( + createDeps({ disabledModels: [{ id: firstModel, label: firstModel }] }), + "anthropic", ); - expect(policy.enabledModels.map((model) => model.id)).toEqual(["model-alpha", "model-gamma"]); + expect(filteredPolicy.enabledModels.some((model) => model.id === firstModel)).toBe(false); }); it("accepts an explicit enabled model", () => { - const result = validateConnectorModelSelection(createDeps(), "claude_compatible", "model-alpha"); + const policy = getConnectorModelPolicy(createDeps(), "anthropic"); + const enabledId = policy.enabledModels[0]?.id; + if (!enabledId) { + throw new Error("anthropic provider should expose at least one model from pi-ai."); + } + const result = validateConnectorModelSelection(createDeps(), "anthropic", enabledId); - expect(result).toEqual({ ok: true, effectiveModel: "model-alpha" }); + expect(result).toEqual({ ok: true, effectiveModel: enabledId }); }); it("rejects an explicit disabled model", () => { + const policy = getConnectorModelPolicy(createDeps(), "anthropic"); + const enabledId = policy.enabledModels[0]?.id; + if (!enabledId) { + throw new Error("anthropic provider should expose at least one model from pi-ai."); + } const result = validateConnectorModelSelection( - createDeps({ disabledModels: [{ id: "model-beta", label: "model-beta" }] }), - "claude_compatible", - "model-beta", + createDeps({ disabledModels: [{ id: enabledId, label: enabledId }] }), + "anthropic", + enabledId, ); expect(result.ok).toBe(false); if (result.ok) { throw new Error("Expected explicit disabled model to be rejected."); } - expect(result.error).toContain('does not allow model "model-beta"'); + expect(result.error).toContain(`does not allow model "${enabledId}"`); }); }); describe("native connector defaults", () => { - it("uses the persisted connector model as the native default when configured", () => { - const codexModels = getConnectorDefinition("codex_local").module.models ?? []; - const configuredDefault = codexModels[1]?.id; - - if (!configuredDefault) { - throw new Error("codex_local requires at least two models for this default policy test."); - } + it("falls back to definition defaultModel when connector model is empty", () => { + const definition = getConnectorDefinition("openai-codex"); + const configuredDefault = definition.defaultModel; const policy = getConnectorModelPolicy({ getConnector: () => ({ - id: "codex_local" as const, + id: "openai-codex" as const, label: "Codex", - model: configuredDefault, + model: null, }), getDisabledModels: () => [], - }, "codex_local"); + }, "openai-codex"); expect(policy.defaultModel).toBe(configuredDefault); expect(validateConnectorModelSelection({ getConnector: () => ({ - id: "codex_local" as const, + id: "openai-codex" as const, label: "Codex", - model: configuredDefault, + model: null, }), getDisabledModels: () => [], - }, "codex_local", null)).toEqual({ + }, "openai-codex", null)).toEqual({ ok: true, effectiveModel: configuredDefault, }); diff --git a/tests/unit/connectors.spec.ts b/tests/unit/connectors.spec.ts index 50d57cda..7e1a1847 100644 --- a/tests/unit/connectors.spec.ts +++ b/tests/unit/connectors.spec.ts @@ -19,16 +19,20 @@ describe("parseEnvBindingText", () => { describe("buildDefaultConnectorRows", () => { it("preserves previously saved connector commands", () => { - const [codex] = buildDefaultConnectorRows([ + const rows = buildDefaultConnectorRows([ { - id: "codex_local", + id: "openai-codex", command: "/usr/local/bin/codex", status: "ready", notes: "custom note", }, ]); + const codex = rows.find((row) => row.id === "openai-codex"); + if (!codex) { + throw new Error("openai-codex connector not found in default rows."); + } - expect(codex.id).toBe("codex_local"); + expect(codex.id).toBe("openai-codex"); expect(codex.command).toBe("/usr/local/bin/codex"); expect(codex.status).toBe("ready"); expect(codex.notes).toBe("custom note"); diff --git a/tests/unit/contracts.test.ts b/tests/unit/contracts.test.ts index 883ef5e5..faa24ea6 100644 --- a/tests/unit/contracts.test.ts +++ b/tests/unit/contracts.test.ts @@ -10,7 +10,7 @@ import { describe("shared contracts", () => { it("fills connector defaults for optional fields", () => { const parsed = connectorInputSchema.parse({ - id: "codex_local", + id: "openai-codex", command: "codex", }); diff --git a/tests/unit/credential-resolver.test.ts b/tests/unit/credential-resolver.test.ts new file mode 100644 index 00000000..98f89c83 --- /dev/null +++ b/tests/unit/credential-resolver.test.ts @@ -0,0 +1,146 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { AppDatabase } from "../../src/main/database"; +import { CredentialStore } from "../../src/main/credentials/credential-store"; +import { CredentialResolver, CredentialError } from "../../src/main/credentials/credential-resolver"; + +beforeAll(() => { + process.env.AGENTCOMPANY_ALLOW_INSECURE_VAULT = "true"; +}); + +describe("CredentialResolver", () => { + let tempDir: string; + let db: AppDatabase; + let store: CredentialStore; + let resolver: CredentialResolver; + + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), "agentcompany-credential-resolver-")); + db = new AppDatabase(tempDir); + db.init(); + store = new CredentialStore({ db }); + resolver = new CredentialResolver(store); + }); + + afterEach(async () => { + db.close(); + await rm(tempDir, { recursive: true, force: true }); + }); + + it("returns the literal apiKey for an api_key connector", async () => { + store.save("anthropic", { + kind: "api_key", + value: { source: "literal", value: "sk-ant-test-1234567890abcd" }, + }); + const resolved = await resolver.resolve("anthropic"); + expect(resolved.apiKey).toBe("sk-ant-test-1234567890abcd"); + expect(resolved.extraEnv).toBeUndefined(); + }); + + it("resolves an api_key connector backed by an env variable", async () => { + process.env.MY_TEST_OPENAI_KEY = "sk-from-env-1234"; + store.save("openai", { + kind: "api_key", + value: { source: "env", value: "MY_TEST_OPENAI_KEY" }, + }); + try { + const resolved = await resolver.resolve("openai"); + expect(resolved.apiKey).toBe("sk-from-env-1234"); + } finally { + delete process.env.MY_TEST_OPENAI_KEY; + } + }); + + it("throws CREDENTIAL_MISSING when the env-backed key is unset", async () => { + delete process.env.UNSET_TEST_KEY; + store.save("xai", { kind: "api_key", value: { source: "env", value: "UNSET_TEST_KEY" } }); + await expect(resolver.resolve("xai")).rejects.toBeInstanceOf(CredentialError); + }); + + it("assembles AWS extraEnv for IAM auth mode", async () => { + store.save("amazon-bedrock", { + kind: "aws", + value: { + authMode: "iam", + region: "us-west-2", + accessKeyId: "AKIA-TEST-FAKE-1234", + secretAccessKey: "secret-fake-secret", + sessionToken: "session-fake-token", + }, + }); + const resolved = await resolver.resolve("amazon-bedrock"); + expect(resolved.apiKey).toBe(""); + expect(resolved.extraEnv).toEqual({ + AWS_REGION: "us-west-2", + AWS_ACCESS_KEY_ID: "AKIA-TEST-FAKE-1234", + AWS_SECRET_ACCESS_KEY: "secret-fake-secret", + AWS_SESSION_TOKEN: "session-fake-token", + }); + }); + + it("assembles AWS extraEnv for profile auth mode plus advanced flags", async () => { + store.save("amazon-bedrock", { + kind: "aws", + value: { + authMode: "profile", + region: "eu-west-1", + awsProfile: "production", + forceCache: true, + endpointUrl: "https://bedrock-runtime.example.com", + skipAuth: true, + forceHttp1: true, + }, + }); + const resolved = await resolver.resolve("amazon-bedrock"); + expect(resolved.extraEnv).toEqual({ + AWS_REGION: "eu-west-1", + AWS_PROFILE: "production", + AWS_BEDROCK_FORCE_CACHE: "1", + AWS_ENDPOINT_URL_BEDROCK_RUNTIME: "https://bedrock-runtime.example.com", + AWS_BEDROCK_SKIP_AUTH: "1", + AWS_BEDROCK_FORCE_HTTP1: "1", + }); + }); + + it("assembles Azure extraEnv with deploymentMap", async () => { + store.save("azure-openai", { + kind: "azure", + value: { + apiKey: "azure-secret-key", + baseUrl: "https://my-resource.openai.azure.com/", + resourceName: "my-resource", + apiVersion: "2024-08-01-preview", + deploymentMap: { "gpt-4o": "gpt4-deploy", "gpt-4o-mini": "gpt4mini-deploy" }, + }, + }); + const resolved = await resolver.resolve("azure-openai"); + expect(resolved.apiKey).toBe("azure-secret-key"); + expect(resolved.extraEnv).toMatchObject({ + AZURE_OPENAI_API_KEY: "azure-secret-key", + AZURE_OPENAI_BASE_URL: "https://my-resource.openai.azure.com/", + AZURE_OPENAI_RESOURCE_NAME: "my-resource", + AZURE_OPENAI_API_VERSION: "2024-08-01-preview", + }); + expect(resolved.extraEnv?.AZURE_OPENAI_DEPLOYMENT_NAME_MAP).toContain("gpt-4o=gpt4-deploy"); + }); + + it("assembles Vertex extraEnv from project + location + credentials path", async () => { + store.save("google-vertex", { + kind: "gcp_adc", + value: { + project: "my-gcp-project", + location: "us-central1", + credentialsPath: "/etc/gcp/service-account.json", + }, + }); + const resolved = await resolver.resolve("google-vertex"); + expect(resolved.apiKey).toBe(""); + expect(resolved.extraEnv).toEqual({ + GOOGLE_CLOUD_PROJECT: "my-gcp-project", + GOOGLE_CLOUD_LOCATION: "us-central1", + GOOGLE_APPLICATION_CREDENTIALS: "/etc/gcp/service-account.json", + }); + }); +}); diff --git a/tests/unit/credential-store.test.ts b/tests/unit/credential-store.test.ts new file mode 100644 index 00000000..85ac3670 --- /dev/null +++ b/tests/unit/credential-store.test.ts @@ -0,0 +1,80 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { AppDatabase } from "../../src/main/database"; +import { CredentialStore } from "../../src/main/credentials/credential-store"; + +beforeAll(() => { + // SecretVault throws unless safeStorage is available or this gate is set. + // safeStorage is unavailable inside vitest, so we use the test-only path. + process.env.AGENTCOMPANY_ALLOW_INSECURE_VAULT = "true"; +}); + +describe("CredentialStore", () => { + let tempDir: string; + let db: AppDatabase; + let store: CredentialStore; + + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), "agentcompany-credential-store-")); + db = new AppDatabase(tempDir); + db.init(); + store = new CredentialStore({ db }); + }); + + afterEach(async () => { + db.close(); + await rm(tempDir, { recursive: true, force: true }); + }); + + it("round-trips an api_key literal credential", () => { + const summary = store.save("anthropic", { kind: "api_key", value: { source: "literal", value: "sk-ant-test1234567890abcd" } }); + expect(summary.kind).toBe("api_key"); + expect(summary.status).toBe("active"); + expect(summary.displayHint).toContain("..."); + expect(summary.displayHint).not.toContain("test1234567890"); + + const decrypted = store.read("anthropic"); + expect(decrypted).not.toBeNull(); + expect(decrypted!.kind).toBe("api_key"); + if (decrypted!.kind === "api_key") { + expect(decrypted!.value.source).toBe("literal"); + expect(decrypted!.value.value).toBe("sk-ant-test1234567890abcd"); + } + }); + + it("round-trips an aws credential with bearer mode", () => { + store.save("amazon-bedrock", { + kind: "aws", + value: { + authMode: "bearer", + region: "us-east-2", + bearerToken: "bedrock-bearer-xyz", + }, + }); + const decrypted = store.read("amazon-bedrock"); + expect(decrypted?.kind).toBe("aws"); + if (decrypted?.kind === "aws") { + expect(decrypted.value.authMode).toBe("bearer"); + expect(decrypted.value.region).toBe("us-east-2"); + expect(decrypted.value.bearerToken).toBe("bedrock-bearer-xyz"); + } + }); + + it("removes a credential", () => { + store.save("openai", { kind: "api_key", value: { source: "literal", value: "sk-test-abcdefghij" } }); + expect(store.list().some((row) => row.connectorId === "openai")).toBe(true); + store.remove("openai"); + expect(store.list().some((row) => row.connectorId === "openai")).toBe(false); + expect(store.read("openai")).toBeNull(); + }); + + it("setStatus updates the status row without losing ciphertext", () => { + store.save("groq", { kind: "api_key", value: { source: "env", value: "GROQ_API_KEY" } }); + store.setStatus("groq", "expired", "refresh failed"); + const row = store.list().find((entry) => entry.connectorId === "groq"); + expect(row?.status).toBe("expired"); + expect(row?.lastError).toBe("refresh failed"); + }); +}); diff --git a/tests/unit/credentials-schemas.test.ts b/tests/unit/credentials-schemas.test.ts new file mode 100644 index 00000000..2b9bb264 --- /dev/null +++ b/tests/unit/credentials-schemas.test.ts @@ -0,0 +1,143 @@ +import { describe, expect, it } from "vitest"; +import { + apiKeyValueSchema, + awsCredentialSchema, + azureCredentialSchema, + credentialPayloadSchema, + gcpAdcCredentialSchema, + removeCredentialInputSchema, + saveCredentialInputSchema, + testCredentialInputSchema, +} from "../../src/shared/contracts/schemas-credentials"; +import { + oauthCancelInputSchema, + oauthManualCodeResponseSchema, + oauthPromptResponseSchema, + oauthStartInputSchema, +} from "../../src/shared/contracts/schemas-oauth"; + +/** Verifies the IPC payload schemas reject malformed input and accept valid + * shapes. These are the only contract between renderer ↔ main for the new + * credential / OAuth channels — incorrect validation would let bad data + * reach pi-mono primitives. */ + +describe("credentials Zod schemas", () => { + it("apiKeyValueSchema rejects empty value", () => { + const result = apiKeyValueSchema.safeParse({ source: "literal", value: "" }); + expect(result.success).toBe(false); + }); + + it("apiKeyValueSchema accepts both source kinds", () => { + expect(apiKeyValueSchema.safeParse({ source: "literal", value: "sk-x" }).success).toBe(true); + expect(apiKeyValueSchema.safeParse({ source: "env", value: "MY_KEY" }).success).toBe(true); + }); + + it("apiKeyValueSchema rejects unknown source", () => { + const result = apiKeyValueSchema.safeParse({ source: "shell", value: "$(echo x)" }); + expect(result.success).toBe(false); + }); + + it("awsCredentialSchema requires authMode", () => { + expect(awsCredentialSchema.safeParse({ region: "us-east-1" }).success).toBe(false); + expect(awsCredentialSchema.safeParse({ authMode: "profile", region: "us-east-1", awsProfile: "default" }).success).toBe(true); + }); + + it("awsCredentialSchema accepts all three auth modes", () => { + for (const authMode of ["profile", "iam", "bearer"] as const) { + const result = awsCredentialSchema.safeParse({ authMode, region: "us-east-1" }); + expect(result.success).toBe(true); + } + }); + + it("azureCredentialSchema requires apiKey", () => { + expect(azureCredentialSchema.safeParse({}).success).toBe(false); + expect(azureCredentialSchema.safeParse({ apiKey: "azure-secret" }).success).toBe(true); + }); + + it("gcpAdcCredentialSchema requires project + location", () => { + expect(gcpAdcCredentialSchema.safeParse({ project: "" }).success).toBe(false); + expect(gcpAdcCredentialSchema.safeParse({ project: "p", location: "us-central1" }).success).toBe(true); + }); + + it("credentialPayloadSchema discriminates on kind", () => { + expect( + credentialPayloadSchema.safeParse({ + kind: "api_key", + value: { source: "literal", value: "sk-x" }, + }).success, + ).toBe(true); + expect( + credentialPayloadSchema.safeParse({ + kind: "azure", + value: { source: "literal", value: "wrong-shape" }, + }).success, + ).toBe(false); + }); + + it("saveCredentialInputSchema requires connectorId + payload", () => { + expect( + saveCredentialInputSchema.safeParse({ + connectorId: "anthropic", + payload: { kind: "api_key", value: { source: "literal", value: "sk-x" } }, + }).success, + ).toBe(true); + expect(saveCredentialInputSchema.safeParse({ connectorId: "" }).success).toBe(false); + expect(saveCredentialInputSchema.safeParse({ payload: {} }).success).toBe(false); + }); + + it("removeCredentialInputSchema rejects empty connectorId", () => { + expect(removeCredentialInputSchema.safeParse({ connectorId: "openai" }).success).toBe(true); + expect(removeCredentialInputSchema.safeParse({ connectorId: "" }).success).toBe(false); + expect(removeCredentialInputSchema.safeParse({}).success).toBe(false); + }); + + it("testCredentialInputSchema rejects empty connectorId", () => { + expect(testCredentialInputSchema.safeParse({ connectorId: "openai" }).success).toBe(true); + expect(testCredentialInputSchema.safeParse({ connectorId: "" }).success).toBe(false); + }); +}); + +describe("oauth Zod schemas", () => { + it("oauthStartInputSchema rejects empty connectorId", () => { + expect(oauthStartInputSchema.safeParse({ connectorId: "claude-pro" }).success).toBe(true); + expect(oauthStartInputSchema.safeParse({ connectorId: "" }).success).toBe(false); + }); + + it("oauthCancelInputSchema rejects missing connectorId", () => { + expect(oauthCancelInputSchema.safeParse({ connectorId: "claude-pro" }).success).toBe(true); + expect(oauthCancelInputSchema.safeParse({}).success).toBe(false); + }); + + it("oauthPromptResponseSchema requires connectorId, requestId and a (possibly empty) value", () => { + expect( + oauthPromptResponseSchema.safeParse({ + connectorId: "github-copilot", + requestId: "req-1", + value: "github.com", + }).success, + ).toBe(true); + expect( + oauthPromptResponseSchema.safeParse({ connectorId: "github-copilot", requestId: "" }).success, + ).toBe(false); + expect( + oauthPromptResponseSchema.safeParse({ connectorId: "github-copilot", value: "x" }).success, + ).toBe(false); + }); + + it("oauthManualCodeResponseSchema requires non-empty code", () => { + expect( + oauthManualCodeResponseSchema.safeParse({ + connectorId: "anthropic", + requestId: "req-1", + code: "abcd", + }).success, + ).toBe(true); + expect( + oauthManualCodeResponseSchema.safeParse({ + connectorId: "anthropic", + requestId: "req-1", + code: "", + }).success, + ).toBe(false); + }); +}); diff --git a/tests/unit/dashboard-charts.test.ts b/tests/unit/dashboard-charts.test.ts index 441e828f..960dc99e 100644 --- a/tests/unit/dashboard-charts.test.ts +++ b/tests/unit/dashboard-charts.test.ts @@ -20,7 +20,7 @@ function makeRun(overrides: Partial = {}): RunRecord { taskId: "task-1", agentId: "agent-1", workspaceId: null, - connectorId: "claude_local", + connectorId: "claude-pro", status: "succeeded", summary: "", errorMessage: null, @@ -47,7 +47,7 @@ function makeCost(overrides: Partial = {}): CostRecord { runId: "run-1", taskId: null, agentId: null, - connectorId: "claude_local", + connectorId: "claude-pro", amountUsd: 0.5, unattributed: false, createdAt: "2026-03-20T10:00:00Z", @@ -67,7 +67,7 @@ function makeAgent(overrides: Partial = {}): AgentRecord { department: "engineering", status: "active", reportsTo: null, - connectorId: "claude_local", + connectorId: "claude-pro", workspaceId: null, model: null, capabilities: "[]", diff --git a/tests/unit/database.test.ts b/tests/unit/database.test.ts index 4cedff0f..3cc6f9ab 100644 --- a/tests/unit/database.test.ts +++ b/tests/unit/database.test.ts @@ -54,7 +54,7 @@ describe("AppDatabase", () => { title: "Lead operator", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: "gpt-5", capabilities: "Directs execution", @@ -95,7 +95,7 @@ describe("AppDatabase", () => { value: "sk-test-secret", }); db.saveConnector({ - id: "codex_local", + id: "openai-codex", command: "codex", model: "gpt-5", envBindingText: "OPENAI_API_KEY=vault-openai", @@ -189,14 +189,14 @@ describe("AppDatabase", () => { title: "Lead", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Leads", budgetMonthlyUsd: 100, }); db.saveConnector({ - id: "codex_local", + id: "openai-codex", command: "codex", model: "gpt-5", envBindingText: "", @@ -217,14 +217,14 @@ describe("AppDatabase", () => { taskId, agentId: leadId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); const { id: runId2 } = db.createRun({ companyId, taskId, agentId: leadId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); expect(db.checkoutTask(taskId, runId1)).toBe(true); @@ -255,7 +255,7 @@ describe("AppDatabase", () => { title: "Budget Owner", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Ships work", @@ -276,7 +276,7 @@ describe("AppDatabase", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); const outcome = db.finishRun({ @@ -310,7 +310,7 @@ describe("AppDatabase", () => { title: "Manager", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: null, model: null, capabilities: "Manages", @@ -323,7 +323,7 @@ describe("AppDatabase", () => { title: "Pending Hire", status: "pending_approval", reportsTo: managerId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: null, model: null, capabilities: "Will execute after approval", @@ -362,7 +362,7 @@ describe("AppDatabase", () => { title: "Active", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: null, model: null, capabilities: "Ships", @@ -375,7 +375,7 @@ describe("AppDatabase", () => { title: "Pending", status: "pending_approval", reportsTo: activeAgentId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: null, model: null, capabilities: "Waiting", @@ -479,7 +479,7 @@ describe("AppDatabase", () => { title: "Test", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: wsId, model: null, capabilities: "test", @@ -495,7 +495,7 @@ describe("AppDatabase", () => { status: "todo", }); - const run = db.createRun({ companyId, taskId, agentId, workspaceId: wsId, connectorId: "codex_local" }); + const run = db.createRun({ companyId, taskId, agentId, workspaceId: wsId, connectorId: "openai-codex" }); await db.appendRunLog(run.id, "stdout", "test log line\n"); // Finish the run with a very old timestamp @@ -544,7 +544,7 @@ describe("AppDatabase", () => { title: "CEO", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: null, model: null, capabilities: "manage", @@ -557,7 +557,7 @@ describe("AppDatabase", () => { title: "IC", status: "pending_approval", reportsTo: managerId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: null, model: null, capabilities: "code", diff --git a/tests/unit/edge-case-budget.test.ts b/tests/unit/edge-case-budget.test.ts index e9a73ac8..4153aa5d 100644 --- a/tests/unit/edge-case-budget.test.ts +++ b/tests/unit/edge-case-budget.test.ts @@ -26,7 +26,7 @@ function scaffold(db: AppDatabase) { status: "active", }); db.saveConnector({ - id: "gemini_local", + id: "gemini-cli", command: "gemini", model: "gemini-pro", envBindingText: "", @@ -47,7 +47,7 @@ function scaffold(db: AppDatabase) { title: "Developer", status: "active", reportsTo: null, - connectorId: "gemini_local", + connectorId: "gemini-cli", workspaceId, model: null, capabilities: "Executes tasks", @@ -134,7 +134,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { taskId, agentId, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); db.checkoutTask(taskId, run1.id); db.markRunStatus(run1.id, "running", ""); @@ -154,7 +154,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { taskId: taskId2, agentId, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); db.checkoutTask(taskId2, run0.id); db.markRunStatus(run0.id, "running", ""); @@ -222,7 +222,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { taskId, agentId, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); db.checkoutTask(taskId, run.id); db.markRunStatus(run.id, "running", ""); @@ -271,7 +271,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { taskId, agentId, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); db.checkoutTask(taskId, run.id); db.markRunStatus(run.id, "running", ""); @@ -329,7 +329,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { taskId, agentId, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); db.checkoutTask(taskId, run.id); db.markRunStatus(run.id, "running", ""); @@ -382,7 +382,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { taskId, agentId, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); db.checkoutTask(taskId, run.id); db.markRunStatus(run.id, "running", ""); @@ -429,7 +429,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { taskId, agentId, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); db.checkoutTask(taskId, run.id); db.markRunStatus(run.id, "running", ""); diff --git a/tests/unit/edge-case-concurrent.test.ts b/tests/unit/edge-case-concurrent.test.ts index 152dccef..c1f4c245 100644 --- a/tests/unit/edge-case-concurrent.test.ts +++ b/tests/unit/edge-case-concurrent.test.ts @@ -37,7 +37,7 @@ function createTestCompany(db: AppDatabase): string { function createTestConnector(db: AppDatabase): void { db.saveConnector({ - id: "gemini_local", + id: "gemini-cli", command: "gemini", model: "gemini-pro", envBindingText: "", @@ -64,7 +64,7 @@ function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string title: name, status: "active", reportsTo: null, - connectorId: "gemini_local", + connectorId: "gemini-cli", workspaceId, model: null, capabilities: "Executes tasks", @@ -116,14 +116,14 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId, agentId: agentA, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); const { id: runIdB } = db.createRun({ companyId, taskId, agentId: agentB, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); // Agent-A claims first @@ -152,14 +152,14 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId: taskId1, agentId: agentA, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); const { id: runIdB1 } = db.createRun({ companyId, taskId: taskId1, agentId: agentB, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); // Agent-A wins first task @@ -174,7 +174,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId: taskId2, agentId: agentB, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); const resultB2 = db.checkoutTask(taskId2, runIdB2); expect(resultB2).toBe(true); @@ -198,7 +198,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId, agentId: agentA, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); expect(db.checkoutTask(taskId, runIdA)).toBe(true); @@ -208,7 +208,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId, agentId: agentB, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); const result = db.checkoutTask(taskId, runIdB); expect(result).toBe(false); @@ -231,7 +231,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId, agentId: agentA, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); expect(db.checkoutTask(taskId, runIdA)).toBe(true); @@ -252,7 +252,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId, agentId: agentB, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); const result = db.checkoutTask(taskId, runIdB); expect(result).toBe(false); @@ -270,7 +270,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId, agentId: agentA, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); expect(db.checkoutTask(taskId, runIdA)).toBe(true); @@ -306,7 +306,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId, agentId: agentA, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); expect(db.checkoutTask(taskId, runIdA)).toBe(true); @@ -328,7 +328,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId, agentId: agentB, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); const result = db.checkoutTask(taskId, runIdB); expect(result).toBe(true); diff --git a/tests/unit/edge-case-session.test.ts b/tests/unit/edge-case-session.test.ts index 90c40e4c..4560c7f1 100644 --- a/tests/unit/edge-case-session.test.ts +++ b/tests/unit/edge-case-session.test.ts @@ -20,16 +20,16 @@ import type { ConnectorId } from "@shared/types"; // Helpers // --------------------------------------------------------------------------- -function scaffold(db: AppDatabase, connectorId: ConnectorId = "gemini_local") { +function scaffold(db: AppDatabase, connectorId: ConnectorId = "gemini-cli") { const companyId = db.saveCompany({ name: "Session Edge Case Co", description: "Session rotation edge case tests (D-05.3)", status: "active", }); - // Always save gemini_local (needed for most tests) + // Always save gemini-cli (needed for most tests) db.saveConnector({ - id: "gemini_local", + id: "gemini-cli", command: "gemini", model: "gemini-pro", envBindingText: "", @@ -119,7 +119,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -135,7 +135,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { expect(session!.runCount).toBe(DEFAULT_THRESHOLDS.maxRuns); // Evaluate rotation -- should trigger - const result = sessionService.evaluateRotation(session!, "gemini_local"); + const result = sessionService.evaluateRotation(session!, "gemini-cli"); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_runs"); }); @@ -148,7 +148,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -162,7 +162,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { const totalTokens = session!.totalInputTokens + session!.totalOutputTokens; expect(totalTokens).toBe(550000); - const result = sessionService.evaluateRotation(session!, "gemini_local"); + const result = sessionService.evaluateRotation(session!, "gemini-cli"); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_tokens"); }); @@ -175,7 +175,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -191,7 +191,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { taskId, agentId, workspaceId, - connectorId: "gemini_local" as ConnectorId, + connectorId: "gemini-cli" as ConnectorId, }); db.finishRun({ runId: run.id, @@ -233,7 +233,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: JSON.stringify({ resumeKey: "abc" }), sessionDisplayId: "gemini-session-1", }); @@ -249,7 +249,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: JSON.stringify({ resumeKey: "abc" }), sessionDisplayId: "gemini-session-2", }); @@ -266,15 +266,15 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { expect(newSession!.totalOutputTokens).toBe(0); }); - it("claude connector is exempt from rotation", () => { - const { companyId, agentId, taskId } = scaffold(db, "gemini_local"); + it("claude connector triggers rotation under pi-mono (no native exemption)", () => { + const { companyId, agentId, taskId } = scaffold(db, "gemini-cli"); const sessionService = createSessionManagementService({ db, logger: mockLogger }); const sessionId = db.createNewSession({ taskId, companyId, agentId, - adapterType: "claude_local", + adapterType: "claude-pro", sessionParamsJson: null, sessionDisplayId: null, }); @@ -285,21 +285,21 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { } const session = db.getActiveTaskSession(taskId)!; - // Per Pitfall 6: claude_local has nativeContextManagement: true, exempt from rotation - const result = sessionService.evaluateRotation(session, "claude_local"); - expect(result.shouldRotate).toBe(false); - expect(result.reason).toBeNull(); + // Pi-mono delegates compaction to pi-agent-core; AgentCompany still applies rotation uniformly. + const result = sessionService.evaluateRotation(session, "claude-pro"); + expect(result.shouldRotate).toBe(true); + expect(result.reason).not.toBeNull(); }); - it("codex connector is exempt from rotation", () => { - const { companyId, agentId, taskId } = scaffold(db, "gemini_local"); + it("codex connector triggers rotation under pi-mono (no native exemption)", () => { + const { companyId, agentId, taskId } = scaffold(db, "gemini-cli"); const sessionService = createSessionManagementService({ db, logger: mockLogger }); const sessionId = db.createNewSession({ taskId, companyId, agentId, - adapterType: "codex_local", + adapterType: "openai-codex", sessionParamsJson: null, sessionDisplayId: null, }); @@ -310,10 +310,10 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { } const session = db.getActiveTaskSession(taskId)!; - // Per Pitfall 6: codex_local has nativeContextManagement: true, exempt from rotation - const result = sessionService.evaluateRotation(session, "codex_local"); - expect(result.shouldRotate).toBe(false); - expect(result.reason).toBeNull(); + // Pi-mono delegates compaction to pi-agent-core; AgentCompany still applies rotation uniformly. + const result = sessionService.evaluateRotation(session, "openai-codex"); + expect(result.shouldRotate).toBe(true); + expect(result.reason).not.toBeNull(); }); it("handoff markdown available for next run context after rotation", () => { @@ -325,7 +325,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -337,7 +337,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { taskId, agentId, workspaceId, - connectorId: "gemini_local" as ConnectorId, + connectorId: "gemini-cli" as ConnectorId, }); db.finishRun({ runId: run.id, diff --git a/tests/unit/heartbeat-coalescing.test.ts b/tests/unit/heartbeat-coalescing.test.ts index 838a930e..2843cf43 100644 --- a/tests/unit/heartbeat-coalescing.test.ts +++ b/tests/unit/heartbeat-coalescing.test.ts @@ -28,7 +28,7 @@ function createTestCompany(db: AppDatabase): string { function createTestConnector(db: AppDatabase): void { db.saveConnector({ - id: "codex_local", + id: "openai-codex", command: "codex", model: "gpt-5", envBindingText: "", @@ -55,7 +55,7 @@ function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string title: name, status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Executes tasks", diff --git a/tests/unit/ipc-agent-graph-handlers.test.ts b/tests/unit/ipc-agent-graph-handlers.test.ts index 73aa346e..2a0da632 100644 --- a/tests/unit/ipc-agent-graph-handlers.test.ts +++ b/tests/unit/ipc-agent-graph-handlers.test.ts @@ -32,13 +32,13 @@ describe("ipc-core-agent-graph-handlers model enforcement", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", lastError: null, }); - db.setDisabledModels("codex_local", [{ id: "gpt-5.4", label: "gpt-5.4" }]); + db.setDisabledModels("openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]); const companyId = db.saveCompany({ name: "Handler Test Co", @@ -60,7 +60,7 @@ describe("ipc-core-agent-graph-handlers model enforcement", () => { title: "Chief Executive Officer", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -93,7 +93,7 @@ describe("ipc-core-agent-graph-handlers model enforcement", () => { department: "executive", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: "gpt-5.4", metadataJson: "{}", diff --git a/tests/unit/ipc-core-admin-handlers.test.ts b/tests/unit/ipc-core-admin-handlers.test.ts index 4c7c9e32..8c01aeb2 100644 --- a/tests/unit/ipc-core-admin-handlers.test.ts +++ b/tests/unit/ipc-core-admin-handlers.test.ts @@ -26,11 +26,17 @@ afterEach(async () => { }); describe("ipc-core-admin-handlers connector saves", () => { - it("accepts compatible connector model catalogs stored as csv", async () => { + it("accepts a valid pi-ai model for a pi-mono connector", async () => { const profileDir = await createTempDir("agentcompany-admin-handler-"); const db = new AppDatabase(profileDir); db.init(); + const { getModels } = await import("@mariozechner/pi-ai"); + const anthropicModel = getModels("anthropic" as never)[0]?.id; + if (!anthropicModel) { + throw new Error("pi-ai anthropic provider must expose at least one model."); + } + const handlers = new Map(); registerCoreAdminHandlers({ registerHandle: (channel, handler) => { @@ -50,16 +56,26 @@ describe("ipc-core-admin-handlers connector saves", () => { throw new Error("saveConnector handler was not registered."); } + // Pi-mono connectors default to an empty command; the schema requires a + // non-empty one, so seed it before hitting the IPC handler. + db.saveConnector({ + id: "anthropic", + command: "pi-mono", + model: null, + envBindingText: "", + notes: "", + }); + const result = await saveConnector(null as unknown as IpcMainInvokeEvent, { - id: "claude_compatible", - command: "claude", - model: "model-alpha,model-beta,model-gamma", + id: "anthropic", + command: "pi-mono", + model: anthropicModel, envBindingText: "ANTHROPIC_BASE_URL=https://example.com/anthropic", - notes: "Compat provider", + notes: "Pi-mono connector", }) as { ok: boolean; error?: { code: string; message: string } }; expect(result.ok).toBe(true); - expect(db.getConnector("claude_compatible").model).toBe("model-alpha,model-beta,model-gamma"); + expect(db.getConnector("anthropic").model).toBe(anthropicModel); db.close(); }); diff --git a/tests/unit/log-redactor.test.ts b/tests/unit/log-redactor.test.ts new file mode 100644 index 00000000..be3650a6 --- /dev/null +++ b/tests/unit/log-redactor.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; +import { redactTokens } from "../../src/main/worker/log-redactor"; + +/** Verifies the worker process token redactor strips known + * credential-shaped substrings from log strings before they leave the + * worker process. */ +describe("worker log redactor", () => { + it("redacts Bearer tokens", () => { + const input = "Authorization: Bearer abc.def.ghi-jkl_mnop"; + expect(redactTokens(input)).toBe("Authorization: Bearer [REDACTED]"); + }); + + it("redacts authorization header values regardless of case", () => { + expect(redactTokens("authorization=secret-value-1234567890")).toContain("[REDACTED]"); + expect(redactTokens("Authorization: secret-value-1234567890")).toContain("[REDACTED]"); + }); + + it("redacts access_token query strings", () => { + expect(redactTokens("https://api.example.com/?access_token=abc123xyz&foo=1")).toBe( + "https://api.example.com/?access_token=[REDACTED]&foo=1", + ); + }); + + it("redacts refresh_token values", () => { + expect(redactTokens("refresh_token=long.refresh.tok&grant_type=refresh_token")).toContain( + "refresh_token=[REDACTED]", + ); + }); + + it("redacts api_key key/value pairs", () => { + const input = "config { api_key: \"sk-test-1234567890abcdef\", model: \"gpt-4o\" }"; + const result = redactTokens(input); + expect(result).not.toContain("sk-test-1234567890abcdef"); + expect(result).toContain("[REDACTED]"); + }); + + it("redacts sk- and sk-ant- prefixed keys anywhere in a string", () => { + expect(redactTokens("error using sk-ant-api03_abcdefghijklmnop, retrying")).toContain("sk-[REDACTED]"); + expect(redactTokens("token=sk-test1234567890_abcd")).toContain("sk-[REDACTED]"); + }); + + it("redacts GitHub personal access token shapes", () => { + expect(redactTokens("git push https://x:ghp_aaaaaaaaaaaaaaaaaa@github.com/foo")).toContain("ghp_[REDACTED]"); + }); + + it("redacts AWS access key id prefixes", () => { + expect(redactTokens("aws cred AKIAIOSFODNN7EXAMPLE in trace")).toContain("AKIA[REDACTED]"); + }); + + it("leaves credential-free strings unchanged", () => { + const safe = "Just a regular log line about model claude-opus-4-5 with usage 123 tokens."; + expect(redactTokens(safe)).toBe(safe); + }); + + it("redacts multiple matches in one string", () => { + const input = "Bearer abc.def.ghi.jkl Authorization=secretsecretsecret"; + const result = redactTokens(input); + expect(result).not.toContain("abc.def.ghi.jkl"); + expect(result).not.toContain("secretsecretsecret"); + }); +}); diff --git a/tests/unit/message-dispatch.test.ts b/tests/unit/message-dispatch.test.ts index aa3c559b..256865e8 100644 --- a/tests/unit/message-dispatch.test.ts +++ b/tests/unit/message-dispatch.test.ts @@ -20,7 +20,7 @@ afterEach(async () => { function createScaffold(db: AppDatabase) { db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -48,7 +48,7 @@ function createScaffold(db: AppDatabase) { title: "Chief Executive Officer", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -61,7 +61,7 @@ function createScaffold(db: AppDatabase) { title: "Chief Technology Officer", status: "idle", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead engineering", @@ -75,7 +75,7 @@ function createScaffold(db: AppDatabase) { title: "Chief Marketing Officer", status: "idle", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead marketing", @@ -89,7 +89,7 @@ function createScaffold(db: AppDatabase) { title: "Pending Hire", status: "pending_approval", reportsTo: ctoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Pending", @@ -103,7 +103,7 @@ function createScaffold(db: AppDatabase) { title: "Former COO", status: "terminated", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "No longer active", diff --git a/tests/unit/message-search.test.ts b/tests/unit/message-search.test.ts index 91412320..5a93a8b7 100644 --- a/tests/unit/message-search.test.ts +++ b/tests/unit/message-search.test.ts @@ -39,7 +39,7 @@ describe("AppDatabase.searchMessages", () => { title: "Senior Engineer", status: "active", reportsTo: null, - connectorId: "claude_local", + connectorId: "claude-pro", workspaceId: null, model: null, capabilities: "coding", @@ -53,7 +53,7 @@ describe("AppDatabase.searchMessages", () => { title: "Lead Designer", status: "active", reportsTo: null, - connectorId: "claude_local", + connectorId: "claude-pro", workspaceId: null, model: null, capabilities: "design", diff --git a/tests/unit/oauth-login-service.test.ts b/tests/unit/oauth-login-service.test.ts new file mode 100644 index 00000000..fedfd0d6 --- /dev/null +++ b/tests/unit/oauth-login-service.test.ts @@ -0,0 +1,64 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { AppDatabase } from "../../src/main/database"; +import { CredentialStore } from "../../src/main/credentials/credential-store"; +import { OAuthLoginService } from "../../src/main/credentials/oauth-login-service"; + +vi.mock("electron", () => ({ + shell: { openExternal: vi.fn(async () => true) }, +})); + +beforeAll(() => { + process.env.AGENTCOMPANY_ALLOW_INSECURE_VAULT = "true"; +}); + +const noopLogger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() }; + +describe("OAuthLoginService", () => { + let tempDir: string; + let db: AppDatabase; + let store: CredentialStore; + let service: OAuthLoginService; + const emit = vi.fn(); + + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), "agentcompany-oauth-service-")); + db = new AppDatabase(tempDir); + db.init(); + store = new CredentialStore({ db }); + service = new OAuthLoginService({ + store, + emitEvent: emit, + refreshConnectorHealth: async () => {}, + logger: noopLogger, + }); + emit.mockClear(); + }); + + afterEach(async () => { + db.close(); + await rm(tempDir, { recursive: true, force: true }); + }); + + it("rejects start() for a non-OAuth connector", async () => { + await expect(service.start("anthropic")).rejects.toThrow(/does not use OAuth/); + }); + + it("rejects start() for an unknown connector id", async () => { + await expect(service.start("definitely-not-a-connector")).rejects.toThrow(/Unknown connector/); + }); + + it("submitPromptResponse returns false when there is no pending prompt", () => { + expect(service.submitPromptResponse("nonexistent-request-id", "value")).toBe(false); + }); + + it("submitManualCode returns false when there is no pending request", () => { + expect(service.submitManualCode("nonexistent-request-id", "code")).toBe(false); + }); + + it("cancel() of a connector with no active flow is a no-op (does not throw)", () => { + expect(() => service.cancel("claude-pro")).not.toThrow(); + }); +}); diff --git a/tests/unit/prompt-manager-classification.test.ts b/tests/unit/prompt-manager-classification.test.ts index 2b6442aa..b5e0cdaa 100644 --- a/tests/unit/prompt-manager-classification.test.ts +++ b/tests/unit/prompt-manager-classification.test.ts @@ -14,7 +14,7 @@ function buildCtx(overrides: Partial = {}): PromptContext { department: "engineering", status: "active", reportsTo: "ceo-1", - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: null, model: null, capabilities: "Lead engineering", @@ -46,7 +46,7 @@ function buildCtx(overrides: Partial = {}): PromptContext { department: "executive", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: null, model: null, capabilities: "Lead the company", diff --git a/tests/unit/session-management.test.ts b/tests/unit/session-management.test.ts index d12d63b6..2b661c59 100644 --- a/tests/unit/session-management.test.ts +++ b/tests/unit/session-management.test.ts @@ -41,7 +41,7 @@ function scaffold(db: AppDatabase) { title: "Developer", status: "active", reportsTo: null, - connectorId: "claude_local", + connectorId: "claude-pro", workspaceId, model: "claude-4-opus", capabilities: "Writes code", @@ -107,7 +107,7 @@ describe("task_sessions CRUD", () => { taskId, companyId, agentId, - adapterType: "claude_local", + adapterType: "claude-pro", sessionParamsJson: JSON.stringify({ sessionId: "abc123" }), sessionDisplayId: "claude-abc123", }); @@ -118,7 +118,7 @@ describe("task_sessions CRUD", () => { expect(session!.taskId).toBe(taskId); expect(session!.companyId).toBe(companyId); expect(session!.agentId).toBe(agentId); - expect(session!.adapterType).toBe("claude_local"); + expect(session!.adapterType).toBe("claude-pro"); expect(session!.sessionParamsJson).toBe(JSON.stringify({ sessionId: "abc123" })); expect(session!.sessionDisplayId).toBe("claude-abc123"); expect(session!.runCount).toBe(0); @@ -135,7 +135,7 @@ describe("task_sessions CRUD", () => { taskId, companyId, agentId, - adapterType: "claude_local", + adapterType: "claude-pro", sessionParamsJson: null, sessionDisplayId: null, }); @@ -163,7 +163,7 @@ describe("task_sessions CRUD", () => { taskId, companyId, agentId, - adapterType: "claude_local", + adapterType: "claude-pro", sessionParamsJson: null, sessionDisplayId: null, }); @@ -183,7 +183,7 @@ describe("task_sessions CRUD", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: JSON.stringify({ key: "val" }), sessionDisplayId: "gemini-session-1", }); @@ -195,14 +195,14 @@ describe("task_sessions CRUD", () => { const session = db.getActiveTaskSession(taskId); expect(session).not.toBeNull(); expect(session!.id).toBe(sessionId); - expect(session!.adapterType).toBe("gemini_local"); + expect(session!.adapterType).toBe("gemini-cli"); }); it("getRecentRunsForTask returns last N runs ordered by finished_at DESC", () => { const { taskId, companyId, agentId, workspaceId } = scaffold(db); // Create some runs - const run1 = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "claude_local" }); + const run1 = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "claude-pro" }); db.finishRun({ runId: run1.id, status: "succeeded", @@ -216,7 +216,7 @@ describe("task_sessions CRUD", () => { unattributedCost: false, }); - const run2 = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "claude_local" }); + const run2 = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "claude-pro" }); db.finishRun({ runId: run2.id, status: "failed", @@ -242,22 +242,25 @@ describe("task_sessions CRUD", () => { }); // =========================================================================== -// adapter exemption +// connector definition lookup // =========================================================================== -describe("adapter exemption", () => { - it("claude_local connector has nativeContextManagement: true", () => { - const def = getConnectorDefinition("claude_local" as ConnectorId); - expect(def.module.nativeContextManagement).toBe(true); +describe("connector definition lookup", () => { + it("resolves claude-pro definition from the pi-mono catalog", () => { + const def = getConnectorDefinition("claude-pro" as ConnectorId); + expect(def.id).toBe("claude-pro"); + expect(def.provider).toBe("anthropic"); }); - it("codex_local connector has nativeContextManagement: true", () => { - const def = getConnectorDefinition("codex_local" as ConnectorId); - expect(def.module.nativeContextManagement).toBe(true); + it("resolves openai-codex definition from the pi-mono catalog", () => { + const def = getConnectorDefinition("openai-codex" as ConnectorId); + expect(def.id).toBe("openai-codex"); + expect(def.provider).toBe("openai-codex"); }); - it("gemini_local connector has nativeContextManagement: false", () => { - const def = getConnectorDefinition("gemini_local" as ConnectorId); - expect(def.module.nativeContextManagement).toBe(false); + it("resolves gemini-cli definition from the pi-mono catalog", () => { + const def = getConnectorDefinition("gemini-cli" as ConnectorId); + expect(def.id).toBe("gemini-cli"); + expect(def.provider).toBe("google-gemini-cli"); }); }); @@ -298,7 +301,7 @@ describe("session management service", () => { companyId: "co-1", taskId: "task-1", agentId: "agent-1", - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, lastRunId: null, @@ -312,7 +315,7 @@ describe("session management service", () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "gemini_local"); + const result = svc.evaluateRotation(session, "gemini-cli"); expect(result.shouldRotate).toBe(false); expect(result.reason).toBeNull(); }); @@ -324,7 +327,7 @@ describe("session management service", () => { companyId: "co-1", taskId: "task-1", agentId: "agent-1", - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, lastRunId: null, @@ -338,7 +341,7 @@ describe("session management service", () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "gemini_local"); + const result = svc.evaluateRotation(session, "gemini-cli"); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_runs"); }); @@ -350,7 +353,7 @@ describe("session management service", () => { companyId: "co-1", taskId: "task-1", agentId: "agent-1", - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, lastRunId: null, @@ -364,7 +367,7 @@ describe("session management service", () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "gemini_local"); + const result = svc.evaluateRotation(session, "gemini-cli"); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_tokens"); }); @@ -377,7 +380,7 @@ describe("session management service", () => { companyId: "co-1", taskId: "task-1", agentId: "agent-1", - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, lastRunId: null, @@ -391,24 +394,24 @@ describe("session management service", () => { createdAt: oldDate, updatedAt: oldDate, }; - const result = svc.evaluateRotation(session, "gemini_local"); + const result = svc.evaluateRotation(session, "gemini-cli"); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_age_hours"); }); }); // ------------------------------------------------------------------------- - // adapter exemption in service + // uniform rotation under pi-mono // ------------------------------------------------------------------------- - describe("adapter exemption in evaluateRotation", () => { - it("returns shouldRotate: false for nativeContextManagement=true even when thresholds exceeded", () => { + describe("uniform rotation under pi-mono", () => { + it("returns shouldRotate: true for all connectors when thresholds exceeded (pi-mono has no native exemption)", () => { const svc = createSessionManagementService({ db, logger: mockLogger }); const session: TaskSessionRecord = { id: "sess-1", companyId: "co-1", taskId: "task-1", agentId: "agent-1", - adapterType: "claude_local", + adapterType: "claude-pro", sessionParamsJson: null, sessionDisplayId: null, lastRunId: null, @@ -422,9 +425,9 @@ describe("session management service", () => { createdAt: new Date(Date.now() - 200 * 60 * 60 * 1000).toISOString(), // Way old updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "claude_local"); - expect(result.shouldRotate).toBe(false); - expect(result.reason).toBeNull(); + const result = svc.evaluateRotation(session, "claude-pro"); + expect(result.shouldRotate).toBe(true); + expect(result.reason).not.toBeNull(); }); }); @@ -439,7 +442,7 @@ describe("session management service", () => { companyId: "co-1", taskId: "task-1", agentId: "agent-1", - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, lastRunId: null, @@ -453,7 +456,7 @@ describe("session management service", () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "gemini_local", { maxRuns: 5 }); + const result = svc.evaluateRotation(session, "gemini-cli", { maxRuns: 5 }); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_runs"); }); @@ -465,7 +468,7 @@ describe("session management service", () => { companyId: "co-1", taskId: "task-1", agentId: "agent-1", - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, lastRunId: null, @@ -479,7 +482,7 @@ describe("session management service", () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "gemini_local", { maxTokens: 100000 }); + const result = svc.evaluateRotation(session, "gemini-cli", { maxTokens: 100000 }); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_tokens"); }); @@ -491,7 +494,7 @@ describe("session management service", () => { companyId: "co-1", taskId: "task-1", agentId: "agent-1", - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, lastRunId: null, @@ -505,7 +508,7 @@ describe("session management service", () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "gemini_local"); + const result = svc.evaluateRotation(session, "gemini-cli"); expect(result.shouldRotate).toBe(false); expect(result.reason).toBeNull(); @@ -526,7 +529,7 @@ describe("session management service", () => { taskId, companyId, agentId, - adapterType: "claude_local", + adapterType: "claude-pro", sessionParamsJson: null, sessionDisplayId: null, }); @@ -552,13 +555,13 @@ describe("session management service", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); // Create a completed run for history - const run = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "gemini_local" as ConnectorId }); + const run = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "gemini-cli" as ConnectorId }); db.finishRun({ runId: run.id, status: "succeeded", @@ -597,7 +600,7 @@ describe("session management service", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: JSON.stringify({ resumeId: "old-abc" }), sessionDisplayId: "gemini-old", }); @@ -644,7 +647,7 @@ describe("session management service", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -663,7 +666,7 @@ describe("session management service", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -684,7 +687,7 @@ describe("session management service", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -773,7 +776,7 @@ describe("wiring integration", () => { db, { id: "task-1" }, { inputTokens: 200, outputTokens: 100, runId: "run-1" }, - "gemini_local", + "gemini-cli", ); expect(sessionService.accumulateTokenUsage).toHaveBeenCalledWith("sess-1", 200, 100, "run-1"); @@ -789,10 +792,10 @@ describe("wiring integration", () => { db, { id: "task-1" }, { inputTokens: 200, outputTokens: 100, runId: "run-1" }, - "gemini_local", + "gemini-cli", ); - expect(sessionService.evaluateRotation).toHaveBeenCalledWith(mockSession, "gemini_local"); + expect(sessionService.evaluateRotation).toHaveBeenCalledWith(mockSession, "gemini-cli"); // evaluateRotation called AFTER accumulateTokenUsage const accOrder = sessionService.accumulateTokenUsage.mock.invocationCallOrder[0]; const evalOrder = sessionService.evaluateRotation.mock.invocationCallOrder[0]; @@ -815,7 +818,7 @@ describe("wiring integration", () => { db, { id: "task-1" }, { inputTokens: 200, outputTokens: 100, runId: "run-1" }, - "gemini_local", + "gemini-cli", ); expect(sessionService.rotateSession).toHaveBeenCalledWith(updatedSession); @@ -832,7 +835,7 @@ describe("wiring integration", () => { db, { id: "task-1" }, { inputTokens: 200, outputTokens: 100, runId: "run-1" }, - "gemini_local", + "gemini-cli", ); expect(sessionService.rotateSession).not.toHaveBeenCalled(); @@ -847,7 +850,7 @@ describe("wiring integration", () => { db, null, { inputTokens: 200, outputTokens: 100, runId: "run-1" }, - "gemini_local", + "gemini-cli", ); expect(db.getActiveTaskSession).not.toHaveBeenCalled(); @@ -863,7 +866,7 @@ describe("wiring integration", () => { db, { id: "task-1" }, { inputTokens: 200, outputTokens: 100, runId: "run-1" }, - "gemini_local", + "gemini-cli", ); expect(sessionService.accumulateTokenUsage).not.toHaveBeenCalled(); @@ -879,7 +882,7 @@ describe("wiring integration", () => { db, { id: "task-1" }, { inputTokens: null, outputTokens: null, runId: "run-1" }, - "gemini_local", + "gemini-cli", ); expect(sessionService.accumulateTokenUsage).toHaveBeenCalledWith("sess-1", 0, 0, "run-1"); diff --git a/tests/unit/skill-injection.test.ts b/tests/unit/skill-injection.test.ts index d2e31be4..24789009 100644 --- a/tests/unit/skill-injection.test.ts +++ b/tests/unit/skill-injection.test.ts @@ -6,7 +6,7 @@ describe("buildConnectorSection", () => { const section = buildConnectorSection({ allConnectors: [ { - id: "codex_local", + id: "openai-codex", label: "Codex", status: "ready", enabledModels: ["gpt-5.4", "gpt-5.4-mini"], @@ -22,7 +22,7 @@ describe("buildConnectorSection", () => { const section = buildConnectorSection({ allConnectors: [ { - id: "codex_local", + id: "openai-codex", label: "Codex", status: "ready", enabledModels: ["gpt-5.4"], @@ -38,7 +38,7 @@ describe("buildConnectorSection", () => { const section = buildConnectorSection({ allConnectors: [ { - id: "codex_local", + id: "openai-codex", label: "Codex", status: "ready", enabledModels: [], diff --git a/tests/unit/skill-types.test.ts b/tests/unit/skill-types.test.ts index 87d49ef6..81ec710d 100644 --- a/tests/unit/skill-types.test.ts +++ b/tests/unit/skill-types.test.ts @@ -17,36 +17,35 @@ import type { describe("skill-types", () => { describe("TOKEN_BUDGETS", () => { - it("has entries for all supported adapter types", () => { - expect(TOKEN_BUDGETS).toHaveProperty("claude_local"); - expect(TOKEN_BUDGETS).toHaveProperty("codex_local"); - expect(TOKEN_BUDGETS).toHaveProperty("gemini_local"); - expect(TOKEN_BUDGETS).toHaveProperty("claude_compatible"); + it("has entries for all supported connector types", () => { + expect(TOKEN_BUDGETS).toHaveProperty("claude-pro"); + expect(TOKEN_BUDGETS).toHaveProperty("anthropic"); + expect(TOKEN_BUDGETS).toHaveProperty("gemini-cli"); + expect(TOKEN_BUDGETS).toHaveProperty("google"); }); - it("has numeric token caps for each adapter", () => { - expect(TOKEN_BUDGETS.claude_local).toBe(50000); - expect(TOKEN_BUDGETS.codex_local).toBe(30000); - expect(TOKEN_BUDGETS.gemini_local).toBe(50000); - expect(TOKEN_BUDGETS.claude_compatible).toBe(50000); + it("has numeric token caps for each connector", () => { + expect(TOKEN_BUDGETS["claude-pro"]).toBe(50000); + expect(TOKEN_BUDGETS["anthropic"]).toBe(50000); + expect(TOKEN_BUDGETS["gemini-cli"]).toBe(50000); + expect(TOKEN_BUDGETS["google"]).toBe(50000); }); }); describe("resolveTokenBudget", () => { - it("returns exact match for known adapter types", () => { - expect(resolveTokenBudget("claude_local")).toBe(50000); - expect(resolveTokenBudget("codex_local")).toBe(30000); - expect(resolveTokenBudget("gemini_local")).toBe(50000); + it("returns exact match for known connector types", () => { + expect(resolveTokenBudget("claude-pro")).toBe(50000); + expect(resolveTokenBudget("anthropic")).toBe(50000); + expect(resolveTokenBudget("gemini-cli")).toBe(50000); }); - it("resolves claude_compat_* variants to claude_compatible budget", () => { - expect(resolveTokenBudget("claude_compat_2")).toBe(50000); - expect(resolveTokenBudget("claude_compat_10")).toBe(50000); - expect(resolveTokenBudget("claude_compatible")).toBe(50000); + it("resolves anthropic-family connectors to the larger injection budget", () => { + expect(resolveTokenBudget("amazon-bedrock")).toBe(50000); + expect(resolveTokenBudget("google-vertex")).toBe(50000); }); - it("returns 30000 default for unknown adapter types", () => { - expect(resolveTokenBudget("unknown_adapter")).toBe(30000); + it("returns 30000 default for unknown connector types", () => { + expect(resolveTokenBudget("unknown_connector")).toBe(30000); }); }); diff --git a/tests/unit/sqlite-session-store.test.ts b/tests/unit/sqlite-session-store.test.ts new file mode 100644 index 00000000..93f3b9b8 --- /dev/null +++ b/tests/unit/sqlite-session-store.test.ts @@ -0,0 +1,80 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import type { AgentMessage } from "@mariozechner/pi-agent-core"; +import { AppDatabase } from "../../src/main/database"; +import { SqliteSessionStore } from "../../src/main/agent-runtime/sqlite-session-store"; + +/** + * Verifies the SqliteSessionStore round-trips pi `AgentMessage` values + * correctly through the new `session_entries` table (migration 27). + */ +describe("SqliteSessionStore", () => { + let tempDir: string; + let db: AppDatabase; + const TASK_SESSION_ID = "task-session-test-1"; + + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), "agentcompany-session-store-")); + db = new AppDatabase(tempDir); + db.init(); + }); + + afterEach(async () => { + db.close(); + await rm(tempDir, { recursive: true, force: true }); + }); + + it("appends and reloads pi user/assistant messages preserving order", () => { + const store = new SqliteSessionStore(db, TASK_SESSION_ID); + const userMessage: AgentMessage = { + role: "user", + content: [{ type: "text", text: "hello" }], + timestamp: Date.now(), + } as AgentMessage; + const assistantMessage: AgentMessage = { + role: "assistant", + content: [{ type: "text", text: "hi" }], + stopReason: "stop", + provider: "anthropic", + model: { id: "claude-opus-4-5" }, + usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, totalTokens: 15, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, + timestamp: Date.now(), + } as unknown as AgentMessage; + store.appendMessage(userMessage); + store.appendMessage(assistantMessage); + const loaded = store.loadMessages(); + expect(loaded).toHaveLength(2); + expect((loaded[0] as { role: string }).role).toBe("user"); + expect((loaded[1] as { role: string }).role).toBe("assistant"); + }); + + it("appends a compaction marker that does not appear as a message", () => { + const store = new SqliteSessionStore(db, TASK_SESSION_ID); + store.appendMessage({ role: "user", content: [{ type: "text", text: "first" }], timestamp: Date.now() } as AgentMessage); + store.appendCompaction("Earlier conversation summarised: planning phase complete."); + const loaded = store.loadMessages(); + expect(loaded).toHaveLength(1); + }); + + it("replaceMessages supersedes prior rows and inserts new ones in order", () => { + const store = new SqliteSessionStore(db, TASK_SESSION_ID); + store.appendMessage({ role: "user", content: [{ type: "text", text: "old-1" }], timestamp: Date.now() } as AgentMessage); + store.appendMessage({ role: "assistant", content: [{ type: "text", text: "old-2" }], timestamp: Date.now() } as AgentMessage); + + const compacted: AgentMessage[] = [ + { role: "user", content: [{ type: "text", text: "context summary" }], timestamp: Date.now() } as AgentMessage, + ]; + store.replaceMessages(compacted, "Summary of prior turns."); + + const active = store.loadMessages(); + expect(active).toHaveLength(1); + expect((active[0] as { content: { text: string }[] }).content[0]?.text).toBe("context summary"); + + // Check: pre-replace messages must still exist as superseded audit rows. + const all = db.sessionEntries.listAll(TASK_SESSION_ID); + const supersededCount = all.filter((row) => row.isSuperseded).length; + expect(supersededCount).toBe(2); + }); +}); diff --git a/tests/unit/store.test.ts b/tests/unit/store.test.ts index 8a3fed56..ca54874b 100644 --- a/tests/unit/store.test.ts +++ b/tests/unit/store.test.ts @@ -285,7 +285,7 @@ const fakeRun = (id: string): RunRecord => ({ taskId: "t-1", agentId: "ag-1", workspaceId: null, - connectorId: "claude_local", + connectorId: "claude-pro", status: "queued", summary: "", errorMessage: null, @@ -311,7 +311,7 @@ const fakeAgent = (id: string, name = "Agent"): AgentRecord => ({ department: "engineering", status: "idle", reportsTo: null, - connectorId: "claude_local", + connectorId: "claude-pro", workspaceId: null, model: null, capabilities: "", diff --git a/tests/unit/wakeup-lifecycle.test.ts b/tests/unit/wakeup-lifecycle.test.ts index 2a19ff99..cc39bd82 100644 --- a/tests/unit/wakeup-lifecycle.test.ts +++ b/tests/unit/wakeup-lifecycle.test.ts @@ -28,7 +28,7 @@ function createTestCompany(db: AppDatabase): string { function createTestConnector(db: AppDatabase): void { db.saveConnector({ - id: "codex_local", + id: "openai-codex", command: "codex", model: "gpt-5", envBindingText: "", @@ -55,7 +55,7 @@ function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string title: name, status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Executes tasks",