@braedonsaunders/appkit-desk 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/backend.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.js","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAA;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,OAAO,EAAE,SAAS,EAAkD,MAAM,QAAQ,CAAA;AAClF,OAAO,EACL,WAAW,EACX,YAAY,EACZ,sBAAsB,EACtB,qBAAqB,GAItB,MAAM,YAAY,CAAA;AAEnB;;;;;;;;;;GAUG;AACH,MAAM,OAAO,2BAA4B,SAAQ,SAAS;IACtC,IAAI,GAAW,6BAA6B,CAAA;CAC/D;AAmHD,MAAM,0BAA0B,GAAG,MAAM,CAAA;AACzC,MAAM,0BAA0B,GAAG,MAAM,CAAA;AACzC,MAAM,8BAA8B,GAAG,GAAG,CAAA;AAC1C,MAAM,4BAA4B,GAAG,KAAK,CAAA;AAC1C,MAAM,2BAA2B,GAAG,MAAM,CAAA;AAC1C,MAAM,gCAAgC,GAAG,GAAG,CAAA;AAC5C;;;;;GAKG;AACH,MAAM,yBAAyB,GAAG,CAAC,CAAA;AACnC,MAAM,qBAAqB,GAAG,KAAK,CAAA;AACnC,MAAM,oBAAoB,GAAG,GAAG,CAAA;AAEhC,MAAM,UAAU,4BAA4B,CAC1C,UAAyC,EAAE;IAE3C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,eAAe,CAAA;IACpD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,cAAc,CAAA;IACjD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAA;IACrD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAA;IACnC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,UAAU,CAAA;IACjD,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,IAAI,0BAA0B,CAAA;IAC/E,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,IAAI,0BAA0B,CAAA;IAC/E,MAAM,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,IAAI,8BAA8B,CAAA;IACzF,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,IAAI,4BAA4B,CAAA;IACrF,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,IAAI,2BAA2B,CAAA;IAClF,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,IAAI,gCAAgC,CAAA;IAC/F,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,qBAAqB,CAAA;IAChE,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,sBAAsB,CAAA;IAEvE,KAAK,UAAU,IAAI,CAAC,IAAoB;QACtC,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;YACzB,MAAM,IAAI,SAAS,CAAC,4CAA4C,QAAQ,GAAG,CAAC,CAAA;QAC9E,CAAC;QACD,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YACxB,MAAM,eAAe,CACnB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE;gBAC9D,QAAQ,EAAE,IAAI,CAAC,gBAAgB;aAChC,CAAC,EACF,6BAA6B,IAAI,CAAC,MAAM,EAAE,CAC3C,CAAA;QACH,CAAC;QAED,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAA;QAC5F,MAAM,GAAG,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;QAC9B,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,CAAA;QAC7B,IAAI,MAAc,CAAA;QAClB,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,gBAAgB,CAAC;gBAC9B,OAAO;gBACP,GAAG;gBACH,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU;gBACjC,IAAI,EAAE,cAAc;gBACpB,SAAS,EAAE,gBAAgB;gBAC3B,YAAY,EAAE,mBAAmB;gBACjC,gBAAgB,EAAE,kBAAkB;gBACpC,MAAM,EAAE,IAAI;aACb,CAAC,CAAA;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,WAAW,CAAC,CAAA;YACzC,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;YACvB,MAAM,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kBAAkB,IAAI,EAAE,CAAA;YAC1D,MAAM,KAAK,YAAY,SAAS;gBAC9B,CAAC,CAAC,IAAI,SAAS,CAAC,GAAG,KAAK,CAAC,OAAO,GAAG,MAAM,EAAE,CAAC;gBAC5C,CAAC,CAAC,IAAI,SAAS,CACX,4CAA4C,IAAI,CAAC,MAAM,KAAK,YAAY,CAAC,KAAK,CAAC,GAAG,MAAM,EAAE,CAC3F,CAAA;QACP,CAAC;QACD,OAAO,aAAa,CAAC;YACnB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,KAAK;YACL,IAAI;YACJ,MAAM;YACN,qEAAqE;YACrE,sEAAsE;YACtE,kCAAkC;YAClC,SAAS,EAAE,CAAC,SAAS,EAAE,EAAE,CACvB,gBAAgB,CAAC;gBACf,OAAO;gBACP,GAAG;gBACH,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU;gBACjC,IAAI,EAAE,cAAc;gBACpB,SAAS,EAAE,iBAAiB;gBAC5B,YAAY,EAAE,qBAAqB;gBACnC,eAAe,EAAE,qBAAqB,GAAG,yBAAyB;gBAClE,gBAAgB,EAAE,kBAAkB;gBACpC,MAAM,EAAE,IAAI;gBACZ,aAAa,EAAE,oBAAoB,IAAI,CAAC,MAAM,UAAU;gBACxD,SAAS;aACV,CAAC;YACJ,iBAAiB;YACjB,GAAG;YACH,SAAS;YACT,gBAAgB;YAChB,WAAW;SACZ,CAAC,CAAA;IACJ,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,CAAA;AACjB,CAAC;AAED,2EAA2E;AAC3E,MAAM,CAAC,MAAM,sBAAsB,GAAgB,4BAA4B,EAAE,CAAA;AAEjF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAS,aAAa,CAAC,OAYtB;IACC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,gBAAgB,EAAE,WAAW,EAAE,iBAAiB,EAAE,GAC7F,OAAO,CAAA;IACT,MAAM,OAAO,GAAG,IAAI,GAAG,EAGpB,CAAA;IACH,0EAA0E;IAC1E,uEAAuE;IACvE,kCAAkC;IAClC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAsC,CAAA;IAC/D,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA0C,CAAA;IAClE,IAAI,MAAM,GAAG,OAAO,CAAC,MAAM,CAAA;IAC3B,IAAI,KAAK,GAAuC,MAAM,CAAA;IACtD,IAAI,UAAU,GAAG,CAAC,CAAA;IAClB,IAAI,YAAY,GAAyB,IAAI,CAAA;IAE7C,SAAS,OAAO,CAAC,KAAY;QAC3B,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YACrC,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YACzB,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACrB,CAAC;QACD,OAAO,CAAC,KAAK,EAAE,CAAA;IACjB,CAAC;IAED,SAAS,QAAQ,CAAC,MAA4B;QAC5C,KAAK,MAAM,OAAO,IAAI,CAAC,GAAG,QAAQ,CAAC;YAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IACtD,CAAC;IAED,SAAS,KAAK,CAAC,KAAgB,EAAE,WAAoB;QACnD,IAAI,KAAK,KAAK,QAAQ;YAAE,OAAM;QAC9B,KAAK,GAAG,QAAQ,CAAA;QAChB,OAAO,CAAC,KAAK,CAAC,CAAA;QACd,MAAM,CAAC,OAAO,EAAE,CAAA;QAChB,yEAAyE;QACzE,uDAAuD;QACvD,IAAI,CAAC,WAAW;YAAE,QAAQ,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;IAC9E,CAAC;IAED,SAAS,MAAM,CAAC,IAAY;QAC1B,MAAM,GAAG,IAAI,CAAA;QACb,wEAAwE;QACxE,sEAAsE;QACtE,MAAM,OAAO,GAAG,IAAI,YAAY,EAAE,CAAA;QAClC,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,KAAK,IAAI,IAAI,KAAK,KAAK,QAAQ,CAAA;QAC3D,MAAM,MAAM,GAAG,CAAC,KAAa,EAAQ,EAAE;YACrC,IAAI,CAAC,OAAO,EAAE;gBAAE,OAAM;YACtB,IAAI,MAAiB,CAAA;YACrB,IAAI,CAAC;gBACH,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YAC9B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,KAAK,CACH,IAAI,SAAS,CAAC,0CAA0C,MAAM,KAAK,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,EACzF,KAAK,CACN,CAAA;gBACD,OAAM;YACR,CAAC;YACD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,IAAI,OAAyB,CAAA;gBAC7B,IAAI,CAAC;oBACH,OAAO,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAA;gBACxC,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,KAAK,CACH,IAAI,SAAS,CAAC,gDAAgD,MAAM,KAAK,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,EAC/F,KAAK,CACN,CAAA;oBACD,OAAM;gBACR,CAAC;gBACD,IAAI,IAAI,IAAI,OAAO,EAAE,CAAC;oBACpB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;oBACrC,IAAI,CAAC,KAAK;wBAAE,SAAQ;oBACpB,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;oBAC1B,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;oBACzB,IAAI,OAAO,CAAC,EAAE;wBAAE,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;;wBACxC,KAAK,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC,6BAA6B,MAAM,KAAK,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;gBAC3F,CAAC;qBAAM,CAAC;oBACN,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,SAAS,CAAC;wBAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;gBAC1D,CAAC;YACH,CAAC;QACH,CAAC,CAAA;QACD,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QACvB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE;YACtB,IAAI,CAAC,IAAI,EAAE,6CAA6C,MAAM,EAAE,CAAC,CAAA;QACnE,CAAC,CAAC,CAAA;QACF,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAY,EAAE,EAAE;YAClC,IAAI,CAAC,IAAI,EAAE,6CAA6C,MAAM,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;QACrF,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,SAAS,IAAI,CAAC,IAAY,EAAE,MAAc;QACxC,IAAI,KAAK,KAAK,MAAM,IAAI,IAAI,KAAK,MAAM;YAAE,OAAM;QAC/C,IAAI,CAAC,OAAO,EAAE,CAAA;QACd,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,wEAAwE;YACxE,qEAAqE;YACrE,uBAAuB;YACvB,KAAK,CAAC,IAAI,SAAS,CAAC,QAAQ,MAAM,4CAA4C,MAAM,IAAI,CAAC,EAAE,KAAK,CAAC,CAAA;YACjG,OAAM;QACR,CAAC;QACD,KAAK,GAAG,cAAc,CAAA;QACtB,MAAM,MAAM,GAAG,GAAG,EAAE,CAAA;QACpB,sEAAsE;QACtE,qEAAqE;QACrE,uEAAuE;QACvE,2EAA2E;QAC3E,0EAA0E;QAC1E,sDAAsD;QACtD,OAAO,CACL,IAAI,2BAA2B,CAC7B,QAAQ,MAAM,oEAAoE;cAC9E,IAAI,MAAM,0CAA0C,CACzD,CACF,CAAA;QACD,YAAY,GAAG,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACxC,CAAC;IAED,mFAAmF;IACnF,KAAK,UAAU,OAAO,CAAC,MAAc,EAAE,MAAc;QACnD,IAAI,IAAY,CAAA;QAChB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,KAAK,KAAK,cAAc,CAAC,CAAA;QAChE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,KAAK,CACH,IAAI,SAAS,CACX,QAAQ,MAAM,+DAA+D,YAAY,CAAC,KAAK,CAAC,EAAE,CACnG,EACD,KAAK,CACN,CAAA;YACD,OAAM;QACR,CAAC;QACD,IAAI,KAAK,KAAK,cAAc,EAAE,CAAC;YAC7B,sEAAsE;YACtE,IAAI,CAAC,OAAO,EAAE,CAAA;YACd,OAAM;QACR,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,CAAA;QACZ,KAAK,GAAG,MAAM,CAAA;QACd,UAAU,IAAI,CAAC,CAAA;QACf,QAAQ,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,MAAM,EAAE,UAAU,EAAE,CAAC,CAAA;IAC5F,CAAC;IAED,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;IAEtB,OAAO;QACL,MAAM;QACN,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,EAAE;YACxC,sEAAsE;YACtE,oEAAoE;YACpE,oEAAoE;YACpE,oEAAoE;YACpE,uBAAuB;YACvB,MAAM,SAAS,GAAG,GAAG,EAAE,GAAG,iBAAiB,CAAA;YAC3C,OAAO,KAAK,KAAK,cAAc,EAAE,CAAC;gBAChC,MAAM,OAAO,GAAG,YAAY,CAAA;gBAC5B,IAAI,CAAC,OAAO,IAAI,GAAG,EAAE,IAAI,SAAS;oBAAE,MAAK;gBACzC,MAAM,OAAO,CAAA;YACf,CAAC;YACD,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;gBACrB,kEAAkE;gBAClE,0DAA0D;gBAC1D,MAAM,IAAI,SAAS,CAAC,QAAQ,MAAM,0BAA0B,CAAC,CAAA;YAC/D,CAAC;YACD,MAAM,EAAE,GAAG,SAAS,EAAE,CAAA;YACtB,MAAM,SAAS,GAAG,cAAc,CAAC,SAAS,IAAI,gBAAgB,CAAA;YAC9D,MAAM,MAAM,GAAG,MAAM,CAAA;YACrB,OAAO,MAAM,IAAI,OAAO,CAAU,CAAC,cAAc,EAAE,aAAa,EAAE,EAAE;gBAClE,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;oBAC5B,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;oBAClB,aAAa,CAAC,IAAI,SAAS,CAAC,iBAAiB,OAAO,CAAC,EAAE,sBAAsB,MAAM,GAAG,CAAC,CAAC,CAAA;gBAC1F,CAAC,EAAE,SAAS,CAAC,CAAA;gBACb,KAAK,CAAC,KAAK,EAAE,CAAA;gBACb,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAA;gBAC1E,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,GAAG,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;YAC/C,CAAC,CAAC,CAAA;QACJ,CAAC;QACD,SAAS,CAAC,QAAQ;YAChB,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;YACvB,OAAO,GAAG,EAAE;gBACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;YAC5B,CAAC,CAAA;QACH,CAAC;QACD,kBAAkB,CAAC,QAAQ;YACzB,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;YACtB,OAAO,GAAG,EAAE;gBACV,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;YAC3B,CAAC,CAAA;QACH,CAAC;QACD,KAAK,CAAC,QAAQ;YACZ,qEAAqE;YACrE,wEAAwE;YACxE,KAAK,CAAC,IAAI,SAAS,CAAC,QAAQ,MAAM,oBAAoB,CAAC,EAAE,IAAI,CAAC,CAAA;YAC9D,MAAM,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,WAAW,CAAC,CAAA;QAC3C,CAAC;KACF,CAAA;AACH,CAAC;AAOD;;;;;;;;;;;;GAYG;AACH,SAAS,WAAW,CAAC,KAAmB,EAAE,KAAK,GAAG,CAAC,GAAG,IAAI;IACxD,IAAI,QAAQ,GAAG,EAAE,CAAA;IACjB,MAAM,MAAM,GAAG,CAAC,KAAc,EAAE,EAAE;QAChC,QAAQ,IAAI,MAAM,CAAC,KAAK,CAAC,CAAA;QACzB,IAAI,QAAQ,CAAC,MAAM,GAAG,KAAK;YAAE,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,KAAK,CAAC,CAAA;IACjF,CAAC,CAAA;IACD,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,6DAA6D;IAC7D,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;IAC1C,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;IAC1C,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAA;AACxC,CAAC;AAED,SAAS,SAAS,CAAC,KAAmB;IACpC,MAAM,OAAO,GAAgB,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,EAAE,CAAA;IACxE,OAAO,CAAC,OAAO,GAAG,IAAI,OAAO,CAAO,CAAC,cAAc,EAAE,EAAE;QACrD,MAAM,MAAM,GAAG,GAAG,EAAE;YAClB,OAAO,CAAC,IAAI,GAAG,IAAI,CAAA;YACnB,cAAc,EAAE,CAAA;QAClB,CAAC,CAAA;QACD,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC1B,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;IAC7B,CAAC,CAAC,CAAA;IACF,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,KAAmB,EAAE,IAAiB,EAAE,WAAmB;IAClF,IAAI,IAAI,CAAC,IAAI;QAAE,OAAM;IACrB,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACrB,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE;QAChC,IAAI,CAAC,IAAI,CAAC,IAAI;YAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACvC,CAAC,EAAE,WAAW,CAAC,CAAA;IACf,SAAS,CAAC,KAAK,EAAE,CAAA;IACjB,MAAM,IAAI,CAAC,OAAO,CAAA;IAClB,YAAY,CAAC,SAAS,CAAC,CAAA;AACzB,CAAC;AAED,SAAS,eAAe,CAAC,KAAmB,EAAE,KAAa;IACzD,OAAO,IAAI,OAAO,CAAO,CAAC,cAAc,EAAE,aAAa,EAAE,EAAE;QACzD,IAAI,MAAM,GAAG,EAAE,CAAA;QACf,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;YACjC,MAAM,IAAI,MAAM,CAAC,KAAK,CAAC,CAAA;QACzB,CAAC,CAAC,CAAA;QACF,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YAC5B,aAAa,CAAC,IAAI,SAAS,CAAC,GAAG,KAAK,qBAAqB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;QAC5E,CAAC,CAAC,CAAA;QACF,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;YAClC,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;gBACf,cAAc,EAAE,CAAA;gBAChB,OAAM;YACR,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,CAAA;YAC5B,aAAa,CACX,IAAI,SAAS,CACX,GAAG,KAAK,qBAAqB,IAAI,IAAI,SAAS,EAAE;kBAC5C,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CACrE,CACF,CAAA;QACH,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,KAAK,UAAU,gBAAgB,CAAC,OAe/B;IACC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS,CAAA;IAClD,IAAI,WAAW,GAAG,gCAAgC,CAAA;IAClD,IAAI,YAAY,GAAG,OAAO,CAAC,YAAY,CAAA;IACvC,OAAO,OAAO,CAAC,GAAG,EAAE,IAAI,QAAQ,EAAE,CAAC;QACjC,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACxB,MAAM,IAAI,SAAS,CAAC,OAAO,CAAC,aAAa,IAAI,gDAAgD,CAAC,CAAA;QAChG,CAAC;QACD,IAAI,OAAO,CAAC,SAAS,EAAE,EAAE,KAAK,IAAI,EAAE,CAAC;YACnC,MAAM,IAAI,SAAS,CAAC,uCAAuC,CAAC,CAAA;QAC9D,CAAC;QACD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,gBAAgB,CACnC,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,UAAU,EAClB,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,gBAAgB,CACzB,CAAA;YACD,IAAI,CAAC;gBACH,MAAM,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;YACtD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,OAAO,EAAE,CAAA;gBAChB,MAAM,KAAK,CAAA;YACb,CAAC;YACD,OAAO,MAAM,CAAA;QACf,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,WAAW,GAAG,YAAY,CAAC,KAAK,CAAC,CAAA;QACnC,CAAC;QACD,MAAM,KAAK,CAAC,YAAY,CAAC,CAAA;QACzB,IAAI,OAAO,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;YAC1C,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,GAAG,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAA;QACpE,CAAC;IACH,CAAC;IACD,MAAM,IAAI,SAAS,CACjB,8CAA8C,OAAO,CAAC,UAAU,KAAK,WAAW,EAAE,CACnF,CAAA;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH;;;;;;;;;;GAUG;AACH,KAAK,UAAU,YAAY,CAAC,MAAc,EAAE,SAAiB;IAC3D,MAAM,OAAO,GAAG,IAAI,YAAY,EAAE,CAAA;IAClC,MAAM,IAAI,OAAO,CAAO,CAAC,cAAc,EAAE,aAAa,EAAE,EAAE;QACxD,IAAI,OAAO,GAAG,KAAK,CAAA;QACnB,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,EAAE;YAC/B,IAAI,OAAO;gBAAE,OAAM;YACnB,OAAO,GAAG,IAAI,CAAA;YACd,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;YAC1B,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;YAC5B,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;YAC3B,IAAI,KAAK;gBAAE,aAAa,CAAC,KAAK,CAAC,CAAA;;gBAC1B,cAAc,EAAE,CAAA;QACvB,CAAC,CAAA;QACD,MAAM,KAAK,GAAG,UAAU,CACtB,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC,mDAAmD,CAAC,CAAC,EAChF,SAAS,CACV,CAAA;QACD,KAAK,CAAC,KAAK,EAAE,EAAE,CAAA;QACf,MAAM,OAAO,GAAG,GAAG,EAAE,CACnB,MAAM,CAAC,IAAI,SAAS,CAAC,wDAAwD,CAAC,CAAC,CAAA;QACjF,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,EAAE;YAC/B,IAAI,MAAiB,CAAA;YACrB,IAAI,CAAC;gBACH,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YAC9B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;gBACjE,OAAM;YACR,CAAC;YACD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,mEAAmE;gBACnE,+CAA+C;gBAC/C,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;oBACvC,MAAM,EAAE,CAAA;oBACR,OAAM;gBACR,CAAC;YACH,CAAC;QACH,CAAC,CAAA;QACD,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QACzB,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QAC7B,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QAC5B,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;IACxD,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,gBAAgB,CACvB,OAAuC,EACvC,UAAkB,EAClB,IAAY,EACZ,gBAAwB;IAExB,OAAO,IAAI,OAAO,CAAS,CAAC,cAAc,EAAE,aAAa,EAAE,EAAE;QAC3D,IAAI,MAAc,CAAA;QAClB,IAAI,CAAC;YACH,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;QAC9B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,aAAa,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;YACxE,OAAM;QACR,CAAC;QACD,IAAI,MAAM,GAAG,EAAE,CAAA;QACf,IAAI,OAAO,GAAG,KAAK,CAAA;QACnB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,IAAI,CAAC,IAAI,SAAS,CAAC,kCAAkC,CAAC,CAAC,CAAA;QACzD,CAAC,EAAE,gBAAgB,CAAC,CAAA;QACpB,8DAA8D;QAC9D,KAAK,CAAC,KAAK,EAAE,EAAE,CAAA;QACf,MAAM,IAAI,GAAG,CAAC,KAAY,EAAE,EAAE;YAC5B,IAAI,OAAO;gBAAE,OAAM;YACnB,OAAO,GAAG,IAAI,CAAA;YACd,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,MAAM,CAAC,OAAO,EAAE,CAAA;YAChB,aAAa,CAAC,KAAK,CAAC,CAAA;QACtB,CAAC,CAAA;QACD,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,EAAE;YAC/B,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;YAClC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;YACpC,IAAI,OAAO,KAAK,CAAC,CAAC,EAAE,CAAC;gBACnB,IAAI,MAAM,CAAC,MAAM,GAAG,oBAAoB,EAAE,CAAC;oBACzC,IAAI,CAAC,IAAI,SAAS,CAAC,gDAAgD,CAAC,CAAC,CAAA;gBACvE,CAAC;gBACD,OAAM;YACR,CAAC;YACD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAA;YACrC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAA;YACtC,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;YAC1B,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;YACzB,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC5B,IAAI,CAAC,IAAI,SAAS,CAAC,gCAAgC,IAAI,EAAE,CAAC,CAAC,CAAA;gBAC3D,OAAM;YACR,CAAC;YACD,OAAO,GAAG,IAAI,CAAA;YACd,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;gBAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAA;YAChE,cAAc,CAAC,MAAM,CAAC,CAAA;QACxB,CAAC,CAAA;QACD,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QACzB,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;QAC1B,sDAAsD;QACtD,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC,wCAAwC,CAAC,CAAC,CAAC,CAAA;QACzF,MAAM,CAAC,KAAK,CAAC,WAAW,IAAI,IAAI,CAAC,CAAA;IACnC,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,eAAe,CACtB,OAAe,EACf,IAAuB,EACvB,OAA4C;IAE5C,OAAO,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE;QAC/B,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;QACjC,GAAG,EAAE,OAAO,CAAC,QAAQ,EAAE,GAAG;QAC1B,GAAG,EAAE,OAAO,CAAC,QAAQ,EAAE,GAAG;KAC3B,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,UAAkB;IACxC,OAAO,gBAAgB,CAAC,UAAU,CAAC,CAAA;AACrC,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAO,CAAC,cAAc,EAAE,EAAE;QAC1C,MAAM,KAAK,GAAG,UAAU,CAAC,cAAc,EAAE,EAAE,CAAC,CAAA;QAC5C,KAAK,CAAC,KAAK,EAAE,CAAA;IACf,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC/D,CAAC","sourcesContent":["/**\n * The backend port and its default Cloud Hypervisor implementation.\n *\n * `DeskBackend` is the seam that keeps the package testable without a\n * hypervisor: the host asks a backend to boot a `DeskLaunchPlan` and gets a\n * `DeskMachine` — a request/response channel to the in-guest agent plus an\n * event subscription. CI and non-KVM developer machines inject an in-memory\n * fake; production uses `cloudHypervisorBackend`, whose process-spawning glue\n * is deliberately thin because everything that can be pure lives in the plan\n * builder and the protocol module.\n */\nimport { spawn, type ChildProcess } from 'node:child_process'\nimport { randomUUID } from 'node:crypto'\nimport { createConnection } from 'node:net'\nimport { Buffer } from 'node:buffer'\nimport type { Duplex } from 'node:stream'\nimport { DeskError, type DeskLaunchPlan, type DeskLauncherIdentity } from './plan'\nimport {\n encodeFrame,\n FrameDecoder,\n GUEST_AGENT_VSOCK_PORT,\n parseHostBoundMessage,\n type GuestCommand,\n type GuestEventMessage,\n type HostBoundMessage,\n} from './protocol'\n\n/**\n * A request that was in flight when the guest connection dropped.\n *\n * Distinct from a plain `DeskError` because the caller's next move is\n * different: this request may ALREADY HAVE RUN. The frame reached the guest,\n * and whether the guest executed it before the channel went away is not\n * knowable from the host. An `exec` may have sent mail; a `click` may have\n * landed. So the transport refuses to guess — it names the ambiguity and lets\n * the caller decide whether the operation is safe to repeat. Anything that\n * rejects with a plain `DeskError` instead never reached the guest at all.\n */\nexport class DeskRequestFateUnknownError extends DeskError {\n override readonly name: string = 'DeskRequestFateUnknownError'\n}\n\n/**\n * What happened to the transport underneath a machine.\n *\n * `reconnected` means the host lost the guest channel mid-lease and got it\n * back; the desk is usable again. It is NOT a promise that the guest is\n * unchanged — see the note on `onConnectionChange`. `lost` means it will not\n * come back on this VM and the desk needs a fresh boot.\n */\nexport type DeskConnectionChange =\n | {\n state: 'reconnected'\n deskId: string\n /** How the previous connection went away. */\n reason: string\n /** Wall-clock time the desk spent with no channel to its guest. */\n downtimeMs: number\n /** Reconnects this machine has survived, including this one. */\n reconnects: number\n }\n | {\n state: 'lost'\n deskId: string\n /** Why it is gone for good: the VMM exited, or the window ran out. */\n reason: string\n }\n\n/** A booted microVM as the host sees it: one desk, one guest-agent channel. */\nexport interface DeskMachine {\n readonly deskId: string\n /**\n * Send one request to the in-guest agent and await its correlated result.\n *\n * Rejects with `DeskError` when the guest reports an error, the request\n * times out, or the connection is gone for good — in that last case the\n * request never left the host, so repeating it is safe.\n *\n * A connection lost mid-lease is re-established rather than fatal: a call\n * that arrives while that is happening WAITS for it instead of failing.\n * A call that was already in flight when the channel dropped rejects with\n * `DeskRequestFateUnknownError`, because the host cannot tell whether the\n * guest ran it.\n */\n request(command: GuestCommand, options?: { timeoutMs?: number }): Promise<unknown>\n /**\n * Subscribe to unsolicited guest events (job exits, frames, focus changes).\n *\n * Subscriptions belong to the machine, not to the socket under it, so they\n * survive a reconnect. What does NOT survive is the guest's own capture\n * state: a guest agent that restarted has no screen, no frame encoder and\n * no video encoder, and a subscriber will simply stop receiving. Watch\n * `onConnectionChange` to notice.\n */\n subscribe(listener: (event: GuestEventMessage) => void): () => void\n /**\n * Watch the transport under this machine.\n *\n * Optional: a backend whose channel cannot drop has nothing to report. The\n * default backend reports every reconnect, because a reconnect that leaves\n * no trace turns a guest agent restarting every few minutes into a desk that\n * misbehaves for reasons nobody can find.\n */\n onConnectionChange?(listener: (change: DeskConnectionChange) => void): () => void\n /** Stop the VM. Idempotent; resolves once the VMM process has exited. */\n shutdown(): Promise<void>\n}\n\nexport interface DeskBackend {\n boot(plan: DeskLaunchPlan): Promise<DeskMachine>\n}\n\nexport type DeskProcessLauncher = (\n command: string,\n args: readonly string[],\n options: { identity?: DeskLauncherIdentity },\n) => ChildProcess\n\nexport interface CloudHypervisorBackendOptions {\n /** Spawns the VMM and image tooling; injectable for tests. */\n launcher?: DeskProcessLauncher\n /** Opens the host side of the vsock Unix socket; injectable for tests. */\n connect?: (socketPath: string) => Duplex\n platform?: NodeJS.Platform\n /**\n * Clock behind every connect and reconnect deadline. Injectable for the same\n * reason `DeskHostOptions.now` is: a test drives a window to its end by\n * moving the clock, rather than sleeping for it and hoping the machine it\n * runs on is fast enough.\n */\n now?: () => number\n idFactory?: () => string\n requestTimeoutMs?: number\n connectTimeoutMs?: number\n connectRetryDelayMs?: number\n /** Bound on a single handshake attempt; see attemptHandshake. */\n handshakeTimeoutMs?: number\n /**\n * How long a machine keeps trying to get its guest channel back after an\n * unexpected drop before declaring the desk lost. Bounded on purpose: the\n * VMM is still alive so a reconnect is cheap and usually fast, but retrying\n * forever against a guest that is never coming back is worse than failing.\n */\n reconnectWindowMs?: number\n /**\n * First delay between reconnect attempts. It doubles toward a ceiling, so\n * this is the fast first poll rather than the steady rate. Separate from\n * `connectRetryDelayMs`, which is sized for a guest that is still cold\n * booting; here the guest was up a moment ago.\n */\n reconnectRetryDelayMs?: number\n killGraceMs?: number\n guestAgentPort?: number\n}\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 30_000\nconst DEFAULT_CONNECT_TIMEOUT_MS = 20_000\nconst DEFAULT_CONNECT_RETRY_DELAY_MS = 100\nconst DEFAULT_HANDSHAKE_TIMEOUT_MS = 5_000\nconst DEFAULT_RECONNECT_WINDOW_MS = 30_000\nconst DEFAULT_RECONNECT_RETRY_DELAY_MS = 250\n/**\n * How far the reconnect delay is allowed to double. A guest agent that systemd\n * is restarting is not helped by being hammered four times a second for the\n * whole window, and an unbounded doubling would spend most of the window\n * asleep after two failures.\n */\nconst RECONNECT_BACKOFF_CEILING = 8\nconst DEFAULT_KILL_GRACE_MS = 2_000\nconst HANDSHAKE_LINE_LIMIT = 128\n\nexport function createCloudHypervisorBackend(\n options: CloudHypervisorBackendOptions = {},\n): DeskBackend {\n const launcher = options.launcher ?? defaultLauncher\n const connect = options.connect ?? defaultConnect\n const platform = options.platform ?? process.platform\n const now = options.now ?? Date.now\n const idFactory = options.idFactory ?? randomUUID\n const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS\n const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS\n const connectRetryDelayMs = options.connectRetryDelayMs ?? DEFAULT_CONNECT_RETRY_DELAY_MS\n const handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS\n const reconnectWindowMs = options.reconnectWindowMs ?? DEFAULT_RECONNECT_WINDOW_MS\n const reconnectRetryDelayMs = options.reconnectRetryDelayMs ?? DEFAULT_RECONNECT_RETRY_DELAY_MS\n const killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS\n const guestAgentPort = options.guestAgentPort ?? GUEST_AGENT_VSOCK_PORT\n\n async function boot(plan: DeskLaunchPlan): Promise<DeskMachine> {\n if (platform !== 'linux') {\n throw new DeskError(`The AppKit desk requires Linux; received ${platform}.`)\n }\n if (plan.overlay.create) {\n await runToCompletion(\n launcher(plan.overlay.create.command, plan.overlay.create.args, {\n identity: plan.launcherIdentity,\n }),\n `overlay creation for desk ${plan.deskId}`,\n )\n }\n\n const child = launcher(plan.vmm.command, plan.vmm.args, { identity: plan.launcherIdentity })\n const log = drainOutput(child)\n const exit = watchExit(child)\n let stream: Duplex\n try {\n stream = await connectWithRetry({\n connect,\n now,\n socketPath: plan.vsock.socketPath,\n port: guestAgentPort,\n timeoutMs: connectTimeoutMs,\n retryDelayMs: connectRetryDelayMs,\n attemptTimeoutMs: handshakeTimeoutMs,\n exited: exit,\n })\n } catch (error) {\n await terminate(child, exit, killGraceMs)\n const said = log.tail()\n const detail = said === '' ? '' : ` The VMM said: ${said}`\n throw error instanceof DeskError\n ? new DeskError(`${error.message}${detail}`)\n : new DeskError(\n `Could not reach the guest agent for desk ${plan.deskId}: ${errorMessage(error)}${detail}`,\n )\n }\n return createMachine({\n deskId: plan.deskId,\n child,\n exit,\n stream,\n // The same path the first connection took, deliberately: a reconnect\n // that skipped confirmGuest would adopt the very hollow handshake the\n // boot path learned not to trust.\n reconnect: (abandoned) =>\n connectWithRetry({\n connect,\n now,\n socketPath: plan.vsock.socketPath,\n port: guestAgentPort,\n timeoutMs: reconnectWindowMs,\n retryDelayMs: reconnectRetryDelayMs,\n maxRetryDelayMs: reconnectRetryDelayMs * RECONNECT_BACKOFF_CEILING,\n attemptTimeoutMs: handshakeTimeoutMs,\n exited: exit,\n exitedMessage: `the VMM for desk ${plan.deskId} exited.`,\n abandoned,\n }),\n reconnectWindowMs,\n now,\n idFactory,\n requestTimeoutMs,\n killGraceMs,\n })\n }\n\n return { boot }\n}\n\n/** The default backend: real Cloud Hypervisor over a real vsock socket. */\nexport const cloudHypervisorBackend: DeskBackend = createCloudHypervisorBackend()\n\n/**\n * The machine, and the small state machine that keeps it connected.\n *\n * A vsock channel is not a fact the host learns once at boot — the transport\n * can lie about being connected, and it can go away mid-lease for reasons that\n * have nothing to do with the desk being unusable: the guest agent restarts,\n * the socat bridge drops, a transient vsock error, the guest wedges for a\n * moment. Treating any of those as terminal stranded the desk for the rest of\n * its lease with a perfectly healthy guest behind it, answering every call\n * with \"no longer connected\". So a live connection is something this machine\n * RE-ESTABLISHES:\n *\n * live ──unexpected drop──▶ reconnecting ──confirmed guest──▶ live\n * │ │\n * │ └──window exhausted / VMM exited──▶ closed\n * └──shutdown(), VMM already exited, protocol violation───────────▶ closed\n *\n * `closed` is terminal. Nothing reconnects out of it, which is what makes\n * `shutdown()` and a dead VMM final rather than the start of a retry loop.\n */\nfunction createMachine(options: {\n deskId: string\n child: ChildProcess\n exit: ExitWatcher\n stream: Duplex\n /** Re-establishes the channel; resolves only once a guest has answered. */\n reconnect: (abandoned: () => boolean) => Promise<Duplex>\n reconnectWindowMs: number\n now: () => number\n idFactory: () => string\n requestTimeoutMs: number\n killGraceMs: number\n}): DeskMachine {\n const { deskId, child, exit, now, idFactory, requestTimeoutMs, killGraceMs, reconnectWindowMs } =\n options\n const pending = new Map<\n string,\n { resolve: (result: unknown) => void; reject: (error: Error) => void; timer: NodeJS.Timeout }\n >()\n // Listeners live on the machine rather than on the socket, so a reconnect\n // re-points the data path at the new stream and every subscriber keeps\n // working without re-registering.\n const listeners = new Set<(event: GuestEventMessage) => void>()\n const watchers = new Set<(change: DeskConnectionChange) => void>()\n let stream = options.stream\n let state: 'live' | 'reconnecting' | 'closed' = 'live'\n let reconnects = 0\n let reconnecting: Promise<void> | null = null\n\n function failAll(error: Error): void {\n for (const entry of pending.values()) {\n clearTimeout(entry.timer)\n entry.reject(error)\n }\n pending.clear()\n }\n\n function announce(change: DeskConnectionChange): void {\n for (const watcher of [...watchers]) watcher(change)\n }\n\n function close(error: DeskError, intentional: boolean): void {\n if (state === 'closed') return\n state = 'closed'\n failAll(error)\n stream.destroy()\n // A desk the consumer asked to stop is not news; one that stopped on its\n // own is the thing an operator needs to be told about.\n if (!intentional) announce({ state: 'lost', deskId, reason: error.message })\n }\n\n function attach(next: Duplex): void {\n stream = next\n // Each connection decodes with its OWN decoder: a frame the drop cut in\n // half must never be completed by the first bytes of its replacement.\n const decoder = new FrameDecoder()\n const current = () => stream === next && state !== 'closed'\n const onData = (chunk: Buffer): void => {\n if (!current()) return\n let values: unknown[]\n try {\n values = decoder.push(chunk)\n } catch (error) {\n close(\n new DeskError(`Guest agent protocol violation on desk ${deskId}: ${errorMessage(error)}`),\n false,\n )\n return\n }\n for (const value of values) {\n let message: HostBoundMessage\n try {\n message = parseHostBoundMessage(value)\n } catch (error) {\n close(\n new DeskError(`Guest agent sent a malformed message on desk ${deskId}: ${errorMessage(error)}`),\n false,\n )\n return\n }\n if ('id' in message) {\n const entry = pending.get(message.id)\n if (!entry) continue\n pending.delete(message.id)\n clearTimeout(entry.timer)\n if (message.ok) entry.resolve(message.result)\n else entry.reject(new DeskError(`Guest agent error on desk ${deskId}: ${message.error}`))\n } else {\n for (const listener of [...listeners]) listener(message)\n }\n }\n }\n next.on('data', onData)\n next.once('close', () => {\n drop(next, `the guest agent connection closed on desk ${deskId}`)\n })\n next.once('error', (error: Error) => {\n drop(next, `the guest agent connection failed on desk ${deskId}: ${error.message}`)\n })\n }\n\n function drop(from: Duplex, reason: string): void {\n if (state !== 'live' || from !== stream) return\n from.destroy()\n if (exit.done) {\n // A dead VMM is not a connection problem. There is no guest to get back\n // to, and retrying against it forever only delays the fresh boot the\n // desk actually needs.\n close(new DeskError(`Desk ${deskId} is no longer connected: the VMM exited (${reason}).`), false)\n return\n }\n state = 'reconnecting'\n const lostAt = now()\n // In-flight requests CANNOT be silently retried. Each of their frames\n // already reached the guest, and whether the guest ran it before the\n // channel went away is not knowable from here: an `exec` may have sent\n // mail, a `click` may have landed. Replaying a side-effecting operation to\n // paper over a blip is the wrong kind of resilience, so every one of them\n // is told its fate is unknown and the caller decides.\n failAll(\n new DeskRequestFateUnknownError(\n `Desk ${deskId} lost its guest agent connection while this request was in flight `\n + `(${reason}); the guest may or may not have run it.`,\n ),\n )\n reconnecting = restore(reason, lostAt)\n }\n\n /** Always resolves: callers await it to learn the outcome, not to be thrown at. */\n async function restore(reason: string, lostAt: number): Promise<void> {\n let next: Duplex\n try {\n next = await options.reconnect(() => state !== 'reconnecting')\n } catch (error) {\n close(\n new DeskError(\n `Desk ${deskId} lost its guest agent connection and could not get it back: ${errorMessage(error)}`,\n ),\n false,\n )\n return\n }\n if (state !== 'reconnecting') {\n // shutdown() won the race while the replacement socket was coming up.\n next.destroy()\n return\n }\n attach(next)\n state = 'live'\n reconnects += 1\n announce({ state: 'reconnected', deskId, reason, downtimeMs: now() - lostAt, reconnects })\n }\n\n attach(options.stream)\n\n return {\n deskId,\n async request(command, requestOptions = {}) {\n // A call that lands mid-reconnect waits for it rather than failing on\n // the spot: the guest is usually back within seconds, and telling a\n // caller a desk is gone when it is about to be fine is the bug this\n // machine exists to end. The wait is bounded by the same window the\n // reconnect itself is.\n const waitUntil = now() + reconnectWindowMs\n while (state === 'reconnecting') {\n const attempt = reconnecting\n if (!attempt || now() >= waitUntil) break\n await attempt\n }\n if (state !== 'live') {\n // This one never left the host, so — unlike a request that was in\n // flight when the channel dropped — repeating it is safe.\n throw new DeskError(`Desk ${deskId} is no longer connected.`)\n }\n const id = idFactory()\n const timeoutMs = requestOptions.timeoutMs ?? requestTimeoutMs\n const target = stream\n return await new Promise<unknown>((resolvePromise, rejectPromise) => {\n const timer = setTimeout(() => {\n pending.delete(id)\n rejectPromise(new DeskError(`Guest request ${command.op} timed out on desk ${deskId}.`))\n }, timeoutMs)\n timer.unref()\n pending.set(id, { resolve: resolvePromise, reject: rejectPromise, timer })\n target.write(encodeFrame({ ...command, id }))\n })\n },\n subscribe(listener) {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n onConnectionChange(listener) {\n watchers.add(listener)\n return () => {\n watchers.delete(listener)\n }\n },\n async shutdown() {\n // Terminal by construction: `closed` is the state the reconnect loop\n // checks, so an explicit shutdown can never be followed by a reconnect.\n close(new DeskError(`Desk ${deskId} is shutting down.`), true)\n await terminate(child, exit, killGraceMs)\n },\n }\n}\n\ninterface ExitWatcher {\n done: boolean\n promise: Promise<void>\n}\n\n/**\n * Read the VMM's stdout and stderr, and keep only the last few kilobytes.\n *\n * This is not for logging — it is load-bearing. The child is spawned with\n * both streams as PIPES, and a pipe nobody reads fills after about 64KB, at\n * which point the VMM BLOCKS on its next write. Cloud Hypervisor logs while\n * it runs, so an unread pipe freezes the guest partway through boot: the\n * process is alive, the disk is untouched, and the guest agent never reaches\n * vsock. Every symptom points at the image and none of them are its fault.\n *\n * Draining costs nothing and buys the tail of what the VMM said, which is the\n * difference between \"the VMM exited\" and knowing why.\n */\nfunction drainOutput(child: ChildProcess, limit = 8 * 1024): { tail: () => string } {\n let buffered = ''\n const absorb = (chunk: unknown) => {\n buffered += String(chunk)\n if (buffered.length > limit) buffered = buffered.slice(buffered.length - limit)\n }\n child.stdout?.on('data', absorb)\n child.stderr?.on('data', absorb)\n // A pipe that errors must not take the process down with it.\n child.stdout?.on('error', () => undefined)\n child.stderr?.on('error', () => undefined)\n return { tail: () => buffered.trim() }\n}\n\nfunction watchExit(child: ChildProcess): ExitWatcher {\n const watcher: ExitWatcher = { done: false, promise: Promise.resolve() }\n watcher.promise = new Promise<void>((resolvePromise) => {\n const settle = () => {\n watcher.done = true\n resolvePromise()\n }\n child.once('exit', settle)\n child.once('error', settle)\n })\n return watcher\n}\n\nasync function terminate(child: ChildProcess, exit: ExitWatcher, killGraceMs: number): Promise<void> {\n if (exit.done) return\n child.kill('SIGTERM')\n const forceKill = setTimeout(() => {\n if (!exit.done) child.kill('SIGKILL')\n }, killGraceMs)\n forceKill.unref()\n await exit.promise\n clearTimeout(forceKill)\n}\n\nfunction runToCompletion(child: ChildProcess, label: string): Promise<void> {\n return new Promise<void>((resolvePromise, rejectPromise) => {\n let stderr = ''\n child.stderr?.on('data', (chunk) => {\n stderr += String(chunk)\n })\n child.once('error', (error) => {\n rejectPromise(new DeskError(`${label} failed to start: ${error.message}`))\n })\n child.once('exit', (code, signal) => {\n if (code === 0) {\n resolvePromise()\n return\n }\n const detail = stderr.trim()\n rejectPromise(\n new DeskError(\n `${label} exited with code ${code ?? 'unknown'}`\n + `${signal ? ` (${signal})` : ''}${detail ? `: ${detail}` : '.'}`,\n ),\n )\n })\n })\n}\n\n/**\n * Cloud Hypervisor exposes guest vsock ports through a host Unix socket with\n * a line-oriented preamble: the client writes `CONNECT <port>\\n` and the VMM\n * answers `OK <assigned>\\n` before the byte stream becomes the guest\n * connection. The VMM creates the socket during boot, so connection attempts\n * retry until the deadline.\n *\n * The same loop serves the first connection and every later reconnect, so a\n * reconnect gets `confirmGuest` too — a mid-lease reconnect that trusted the\n * banner would re-adopt exactly the hollow stream the boot path learned not to.\n */\nasync function connectWithRetry(options: {\n connect: (socketPath: string) => Duplex\n now: () => number\n socketPath: string\n port: number\n timeoutMs: number\n retryDelayMs: number\n /** Ceiling the delay doubles toward; omitted keeps the delay constant. */\n maxRetryDelayMs?: number\n attemptTimeoutMs: number\n exited: ExitWatcher\n /** A dead VMM means something different at boot than it does mid-lease. */\n exitedMessage?: string\n /** Stops the loop early when the caller no longer wants the connection. */\n abandoned?: () => boolean\n}): Promise<Duplex> {\n const deadline = options.now() + options.timeoutMs\n let lastFailure = 'the guest agent never answered'\n let retryDelayMs = options.retryDelayMs\n while (options.now() <= deadline) {\n if (options.exited.done) {\n throw new DeskError(options.exitedMessage ?? 'The VMM exited before the guest agent came up.')\n }\n if (options.abandoned?.() === true) {\n throw new DeskError('the connection attempt was abandoned.')\n }\n try {\n const stream = await attemptHandshake(\n options.connect,\n options.socketPath,\n options.port,\n options.attemptTimeoutMs,\n )\n try {\n await confirmGuest(stream, options.attemptTimeoutMs)\n } catch (error) {\n stream.destroy()\n throw error\n }\n return stream\n } catch (error) {\n lastFailure = errorMessage(error)\n }\n await delay(retryDelayMs)\n if (options.maxRetryDelayMs !== undefined) {\n retryDelayMs = Math.min(retryDelayMs * 2, options.maxRetryDelayMs)\n }\n }\n throw new DeskError(\n `Timed out connecting to the guest agent at ${options.socketPath}: ${lastFailure}`,\n )\n}\n\n/**\n * One handshake attempt, which MUST settle.\n *\n * Cloud Hypervisor accepts a connection on the vsock socket whether or not\n * anything in the guest is listening on the port, and when nothing is it may\n * answer nothing at all: no reply, no error, no close. Without a deadline of\n * its own this promise then never settles — and because the caller awaits it\n * inside the retry loop, the loop stops iterating, the overall deadline is\n * never re-checked, and the boot hangs forever rather than failing. That is\n * indistinguishable from a wedged host and it is why every attempt is bounded\n * here as well as in aggregate.\n */\n/**\n * Prove a guest is behind the handshake by asking it something.\n *\n * Cloud Hypervisor answers `CONNECT <port>` with `OK` whether or not anything\n * in the guest is listening on that port, and only closes the socket a moment\n * later when it finds nobody there. A caller that trusts the banner therefore\n * \"connects\" to a guest that is still booting, gets a closed stream seconds\n * later, and — because a closed machine is not retried — leaves the desk dead\n * for as long as it is leased, with a healthy guest sitting behind it. So the\n * handshake is not the test; a reply is.\n */\nasync function confirmGuest(stream: Duplex, timeoutMs: number): Promise<void> {\n const decoder = new FrameDecoder()\n await new Promise<void>((resolvePromise, rejectPromise) => {\n let settled = false\n const finish = (error?: Error) => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n stream.off('data', onData)\n stream.off('close', onClose)\n stream.off('error', finish)\n if (error) rejectPromise(error)\n else resolvePromise()\n }\n const timer = setTimeout(\n () => finish(new DeskError('the guest agent did not answer the first request.')),\n timeoutMs,\n )\n timer.unref?.()\n const onClose = () =>\n finish(new DeskError('the connection closed before the guest agent answered.'))\n const onData = (chunk: Buffer) => {\n let values: unknown[]\n try {\n values = decoder.push(chunk)\n } catch (error) {\n finish(error instanceof Error ? error : new Error(String(error)))\n return\n }\n for (const value of values) {\n // Anything at all proves a guest is there; the reply is parsed for\n // real by the machine once it owns the stream.\n if (value && typeof value === 'object') {\n finish()\n return\n }\n }\n }\n stream.on('data', onData)\n stream.once('close', onClose)\n stream.once('error', finish)\n stream.write(encodeFrame({ id: 'hello', op: 'ping' }))\n })\n}\n\nfunction attemptHandshake(\n connect: (socketPath: string) => Duplex,\n socketPath: string,\n port: number,\n attemptTimeoutMs: number,\n): Promise<Duplex> {\n return new Promise<Duplex>((resolvePromise, rejectPromise) => {\n let stream: Duplex\n try {\n stream = connect(socketPath)\n } catch (error) {\n rejectPromise(error instanceof Error ? error : new Error(String(error)))\n return\n }\n let banner = ''\n let settled = false\n const timer = setTimeout(() => {\n fail(new DeskError('vsock handshake went unanswered.'))\n }, attemptTimeoutMs)\n // An unref'd timer must not hold the process open on its own.\n timer.unref?.()\n const fail = (error: Error) => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n stream.destroy()\n rejectPromise(error)\n }\n const onData = (chunk: Buffer) => {\n banner += chunk.toString('latin1')\n const newline = banner.indexOf('\\n')\n if (newline === -1) {\n if (banner.length > HANDSHAKE_LINE_LIMIT) {\n fail(new DeskError('vsock handshake reply exceeded the line limit.'))\n }\n return\n }\n const line = banner.slice(0, newline)\n const rest = banner.slice(newline + 1)\n stream.off('data', onData)\n stream.off('error', fail)\n if (!line.startsWith('OK ')) {\n fail(new DeskError(`vsock handshake was refused: ${line}`))\n return\n }\n settled = true\n clearTimeout(timer)\n if (rest.length > 0) stream.unshift(Buffer.from(rest, 'latin1'))\n resolvePromise(stream)\n }\n stream.on('data', onData)\n stream.once('error', fail)\n // A close before the banner is a refusal, not a hang.\n stream.once('close', () => fail(new DeskError('vsock handshake closed before a reply.')))\n stream.write(`CONNECT ${port}\\n`)\n })\n}\n\nfunction defaultLauncher(\n command: string,\n args: readonly string[],\n options: { identity?: DeskLauncherIdentity },\n): ChildProcess {\n return spawn(command, [...args], {\n stdio: ['ignore', 'pipe', 'pipe'],\n uid: options.identity?.uid,\n gid: options.identity?.gid,\n })\n}\n\nfunction defaultConnect(socketPath: string): Duplex {\n return createConnection(socketPath)\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise<void>((resolvePromise) => {\n const timer = setTimeout(resolvePromise, ms)\n timer.unref()\n })\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import type { DeskBackend } from './backend.js';
2
+ import type { DeskLaunchPlan } from './plan.js';
3
+ export type DeskBackendConformanceReport = {
4
+ deskId: string;
5
+ ping: true;
6
+ capabilities: {
7
+ virtioGpu: boolean;
8
+ };
9
+ shutdownIdempotent: true;
10
+ rejectsAfterShutdown: true;
11
+ };
12
+ /**
13
+ * Executable behavioral contract for every Desk backend. It boots a disposable
14
+ * plan, verifies the request channel and terminal shutdown semantics, and
15
+ * always tears the machine down. Provider packages can run this unchanged in
16
+ * CI rather than maintaining lookalike test suites.
17
+ */
18
+ export declare function verifyDeskBackendConformance(backend: DeskBackend, plan: DeskLaunchPlan): Promise<DeskBackendConformanceReport>;
19
+ //# sourceMappingURL=conformance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAG5C,MAAM,MAAM,4BAA4B,GAAG;IACzC,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,IAAI,CAAA;IACV,YAAY,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,CAAA;IACpC,kBAAkB,EAAE,IAAI,CAAA;IACxB,oBAAoB,EAAE,IAAI,CAAA;CAC3B,CAAA;AAED;;;;;GAKG;AACH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,WAAW,EACpB,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,4BAA4B,CAAC,CA4BvC"}
package/conformance.js ADDED
@@ -0,0 +1,46 @@
1
+ import { parseCapabilities } from './protocol.js';
2
+ /**
3
+ * Executable behavioral contract for every Desk backend. It boots a disposable
4
+ * plan, verifies the request channel and terminal shutdown semantics, and
5
+ * always tears the machine down. Provider packages can run this unchanged in
6
+ * CI rather than maintaining lookalike test suites.
7
+ */
8
+ export async function verifyDeskBackendConformance(backend, plan) {
9
+ const machine = await backend.boot(plan);
10
+ let firstShutdown = false;
11
+ try {
12
+ if (machine.deskId !== plan.deskId)
13
+ throw new Error('Desk backend returned a machine for the wrong desk.');
14
+ const ping = await machine.request({ op: 'ping' });
15
+ if (!isPong(ping))
16
+ throw new Error('Desk backend did not return a valid ping response.');
17
+ const capabilities = parseCapabilities(await machine.request({ op: 'capabilities' }));
18
+ await machine.shutdown();
19
+ firstShutdown = true;
20
+ await machine.shutdown();
21
+ let rejectsAfterShutdown = false;
22
+ try {
23
+ await machine.request({ op: 'ping' });
24
+ }
25
+ catch {
26
+ rejectsAfterShutdown = true;
27
+ }
28
+ if (!rejectsAfterShutdown)
29
+ throw new Error('Desk backend accepted a request after shutdown.');
30
+ return {
31
+ deskId: machine.deskId,
32
+ ping: true,
33
+ capabilities,
34
+ shutdownIdempotent: true,
35
+ rejectsAfterShutdown: true,
36
+ };
37
+ }
38
+ finally {
39
+ if (!firstShutdown)
40
+ await machine.shutdown().catch(() => undefined);
41
+ }
42
+ }
43
+ function isPong(value) {
44
+ return typeof value === 'object' && value !== null && value.pong === true;
45
+ }
46
+ //# sourceMappingURL=conformance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conformance.js","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAU9C;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,OAAoB,EACpB,IAAoB;IAEpB,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACxC,IAAI,aAAa,GAAG,KAAK,CAAA;IACzB,IAAI,CAAC;QACH,IAAI,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAA;QAC1G,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;QAClD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAA;QACxF,MAAM,YAAY,GAAG,iBAAiB,CAAC,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,cAAc,EAAE,CAAC,CAAC,CAAA;QACrF,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAA;QACxB,aAAa,GAAG,IAAI,CAAA;QACpB,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAA;QACxB,IAAI,oBAAoB,GAAG,KAAK,CAAA;QAChC,IAAI,CAAC;YACH,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;QACvC,CAAC;QAAC,MAAM,CAAC;YACP,oBAAoB,GAAG,IAAI,CAAA;QAC7B,CAAC;QACD,IAAI,CAAC,oBAAoB;YAAE,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAA;QAC7F,OAAO;YACL,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,IAAI,EAAE,IAAI;YACV,YAAY;YACZ,kBAAkB,EAAE,IAAI;YACxB,oBAAoB,EAAE,IAAI;SAC3B,CAAA;IACH,CAAC;YAAS,CAAC;QACT,IAAI,CAAC,aAAa;YAAE,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;IACrE,CAAC;AACH,CAAC;AAED,SAAS,MAAM,CAAC,KAAc;IAC5B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAK,KAA4B,CAAC,IAAI,KAAK,IAAI,CAAA;AACnG,CAAC","sourcesContent":["import type { DeskBackend } from './backend'\nimport type { DeskLaunchPlan } from './plan'\nimport { parseCapabilities } from './protocol'\n\nexport type DeskBackendConformanceReport = {\n deskId: string\n ping: true\n capabilities: { virtioGpu: boolean }\n shutdownIdempotent: true\n rejectsAfterShutdown: true\n}\n\n/**\n * Executable behavioral contract for every Desk backend. It boots a disposable\n * plan, verifies the request channel and terminal shutdown semantics, and\n * always tears the machine down. Provider packages can run this unchanged in\n * CI rather than maintaining lookalike test suites.\n */\nexport async function verifyDeskBackendConformance(\n backend: DeskBackend,\n plan: DeskLaunchPlan,\n): Promise<DeskBackendConformanceReport> {\n const machine = await backend.boot(plan)\n let firstShutdown = false\n try {\n if (machine.deskId !== plan.deskId) throw new Error('Desk backend returned a machine for the wrong desk.')\n const ping = await machine.request({ op: 'ping' })\n if (!isPong(ping)) throw new Error('Desk backend did not return a valid ping response.')\n const capabilities = parseCapabilities(await machine.request({ op: 'capabilities' }))\n await machine.shutdown()\n firstShutdown = true\n await machine.shutdown()\n let rejectsAfterShutdown = false\n try {\n await machine.request({ op: 'ping' })\n } catch {\n rejectsAfterShutdown = true\n }\n if (!rejectsAfterShutdown) throw new Error('Desk backend accepted a request after shutdown.')\n return {\n deskId: machine.deskId,\n ping: true,\n capabilities,\n shutdownIdempotent: true,\n rejectsAfterShutdown: true,\n }\n } finally {\n if (!firstShutdown) await machine.shutdown().catch(() => undefined)\n }\n}\n\nfunction isPong(value: unknown): value is { pong: true } {\n return typeof value === 'object' && value !== null && (value as { pong?: unknown }).pong === true\n}\n"]}
package/events.d.ts ADDED
@@ -0,0 +1,190 @@
1
+ /**
2
+ * The typed desk event stream and the ports a consumer supplies.
3
+ *
4
+ * AppKit owns the mechanism of running a desk; the consumer owns the record.
5
+ * Every governed thing that happens on a desk is delivered to `onEvent` as one
6
+ * member of a closed union so the consumer's append-only ledger can persist it
7
+ * without inventing its own taxonomy. Handover boundaries additionally reach
8
+ * the `audit` port — and, while a handover is active, they are the *only*
9
+ * thing that reaches any port. See the masking contract on `DeskPorts`.
10
+ */
11
+ export type DeskPointerButton = 'left' | 'middle' | 'right';
12
+ export interface DeskPoint {
13
+ x: number;
14
+ y: number;
15
+ }
16
+ export interface WindowInfo {
17
+ id: string;
18
+ title: string;
19
+ appId: string | null;
20
+ }
21
+ /**
22
+ * One node of the focused application's AT-SPI accessibility tree, when the
23
+ * application exposes one. Perception is pixels-primary; the tree is
24
+ * opportunistic and may be `null` at any time.
25
+ */
26
+ export interface A11yNode {
27
+ id: string;
28
+ role: string;
29
+ name: string | null;
30
+ actions: readonly string[];
31
+ bounds: {
32
+ x: number;
33
+ y: number;
34
+ width: number;
35
+ height: number;
36
+ } | null;
37
+ children: readonly A11yNode[];
38
+ }
39
+ /**
40
+ * How the pixels of a live frame are encoded on the wire.
41
+ *
42
+ * Two jobs, two formats. `observe()` is always lossless PNG because it feeds a
43
+ * model's vision and anchors coordinates, and it is asked for rarely. The
44
+ * `frames()` stream feeds a human driving the screen: there the scarce
45
+ * resource is bytes on the transport, not fidelity, so a lossy `jpeg` is
46
+ * roughly an order of magnitude smaller per frame and is what makes a live
47
+ * view feel like a remote desktop rather than a slideshow.
48
+ */
49
+ export type DeskFrameFormat = 'png' | 'jpeg';
50
+ /**
51
+ * The two kinds of unit a video stream is made of.
52
+ *
53
+ * `init` is the header a decoder needs before it can make sense of anything —
54
+ * for fragmented MP4, `ftyp` plus `moov`. It arrives once per encoder start and
55
+ * a consumer that never receives it decodes nothing at all, silently. `media`
56
+ * is one fragment of pictures. Ordering is load-bearing: init first, then media
57
+ * from a keyframe onwards.
58
+ */
59
+ export type DeskVideoChunkKind = 'init' | 'media';
60
+ export type DeskHandoverScope = 'view' | 'control';
61
+ export type DeskHandoverEndReason = 'ended' | 'expired' | 'revoked';
62
+ /**
63
+ * The closed union of recordable desk activity. The consumer's ledger stores
64
+ * these rows; the union is defined here so the mechanism and the record agree
65
+ * on the taxonomy without the package ever touching a database.
66
+ */
67
+ export type DeskEventDetail = {
68
+ kind: 'shell_command';
69
+ command: string;
70
+ args: readonly string[];
71
+ cwd: string | null;
72
+ exitCode: number | null;
73
+ signal: string | null;
74
+ } | {
75
+ kind: 'app_launch';
76
+ appId: string;
77
+ args: readonly string[];
78
+ } | {
79
+ kind: 'click';
80
+ x: number;
81
+ y: number;
82
+ button: DeskPointerButton;
83
+ } | {
84
+ kind: 'type';
85
+ text: string;
86
+ } | {
87
+ kind: 'key';
88
+ combo: string;
89
+ } | {
90
+ kind: 'scroll';
91
+ x: number;
92
+ y: number;
93
+ dx: number;
94
+ dy: number;
95
+ } | {
96
+ kind: 'drag';
97
+ from: DeskPoint;
98
+ to: DeskPoint;
99
+ } | {
100
+ kind: 'window_focus';
101
+ window: WindowInfo;
102
+ } | {
103
+ kind: 'screen_open';
104
+ width: number;
105
+ height: number;
106
+ } | {
107
+ kind: 'screen_close';
108
+ } | {
109
+ kind: 'handover_begin';
110
+ actor: string | null;
111
+ scope: DeskHandoverScope;
112
+ } | {
113
+ kind: 'handover_end';
114
+ actor: string | null;
115
+ durationMs: number;
116
+ } | {
117
+ kind: 'job_start';
118
+ jobId: string;
119
+ command: string;
120
+ args: readonly string[];
121
+ } | {
122
+ kind: 'job_exit';
123
+ jobId: string;
124
+ exitCode: number | null;
125
+ signal: string | null;
126
+ };
127
+ export type DeskEventKind = DeskEventDetail['kind'];
128
+ export type DeskEvent = DeskEventDetail & {
129
+ deskId: string;
130
+ at: string;
131
+ };
132
+ /**
133
+ * Handover boundaries as the audit port sees them: who took over, under what
134
+ * scope, and for how long — never what they typed.
135
+ */
136
+ export type DeskAuditEntry = {
137
+ kind: 'handover_begin';
138
+ deskId: string;
139
+ actor: string | null;
140
+ scope: DeskHandoverScope;
141
+ ttlMs: number;
142
+ at: string;
143
+ } | {
144
+ kind: 'handover_end';
145
+ deskId: string;
146
+ actor: string | null;
147
+ durationMs: number;
148
+ reason: DeskHandoverEndReason;
149
+ at: string;
150
+ };
151
+ /**
152
+ * Consumer-supplied policy. Absent hooks allow: the package is mechanism, and
153
+ * a deployment that wants governance supplies it here. A hook returning
154
+ * `false` makes the corresponding call throw before anything reaches the
155
+ * guest.
156
+ */
157
+ export interface DeskPolicyPort {
158
+ allowExec?(request: {
159
+ deskId: string;
160
+ command: string;
161
+ args: readonly string[];
162
+ keepAlive: boolean;
163
+ }): boolean;
164
+ allowScreen?(request: {
165
+ deskId: string;
166
+ width: number;
167
+ height: number;
168
+ }): boolean;
169
+ allowHandover?(request: {
170
+ deskId: string;
171
+ scope: DeskHandoverScope;
172
+ ttlMs: number;
173
+ }): boolean;
174
+ }
175
+ /**
176
+ * The ports a consumer supplies to `createDeskHost`.
177
+ *
178
+ * Masking contract (load-bearing): while a handover is active, input events —
179
+ * `click`, `type`, `key`, `scroll`, `drag` — and the screen-derived events
180
+ * `window_focus` and `app_launch` never reach `onEvent`, and frames are never
181
+ * emitted to `frames()` consumers. Only `handover_begin` and `handover_end`
182
+ * cross the boundary, carrying actor and duration. Getting this wrong leaks
183
+ * credentials into an append-only record.
184
+ */
185
+ export interface DeskPorts {
186
+ policy?: DeskPolicyPort;
187
+ onEvent: (event: DeskEvent) => void;
188
+ audit: (entry: DeskAuditEntry) => void;
189
+ }
190
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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"}
package/events.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The typed desk event stream and the ports a consumer supplies.
3
+ *
4
+ * AppKit owns the mechanism of running a desk; the consumer owns the record.
5
+ * Every governed thing that happens on a desk is delivered to `onEvent` as one
6
+ * member of a closed union so the consumer's append-only ledger can persist it
7
+ * without inventing its own taxonomy. Handover boundaries additionally reach
8
+ * the `audit` port — and, while a handover is active, they are the *only*
9
+ * thing that reaches any port. See the masking contract on `DeskPorts`.
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=events.js.map
package/events.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG","sourcesContent":["/**\n * The typed desk event stream and the ports a consumer supplies.\n *\n * AppKit owns the mechanism of running a desk; the consumer owns the record.\n * Every governed thing that happens on a desk is delivered to `onEvent` as one\n * member of a closed union so the consumer's append-only ledger can persist it\n * without inventing its own taxonomy. Handover boundaries additionally reach\n * the `audit` port — and, while a handover is active, they are the *only*\n * thing that reaches any port. See the masking contract on `DeskPorts`.\n */\n\nexport type DeskPointerButton = 'left' | 'middle' | 'right'\n\nexport interface DeskPoint {\n x: number\n y: number\n}\n\nexport interface WindowInfo {\n id: string\n title: string\n appId: string | null\n}\n\n/**\n * One node of the focused application's AT-SPI accessibility tree, when the\n * application exposes one. Perception is pixels-primary; the tree is\n * opportunistic and may be `null` at any time.\n */\nexport interface A11yNode {\n id: string\n role: string\n name: string | null\n actions: readonly string[]\n bounds: { x: number; y: number; width: number; height: number } | null\n children: readonly A11yNode[]\n}\n\n/**\n * How the pixels of a live frame are encoded on the wire.\n *\n * Two jobs, two formats. `observe()` is always lossless PNG because it feeds a\n * model's vision and anchors coordinates, and it is asked for rarely. The\n * `frames()` stream feeds a human driving the screen: there the scarce\n * resource is bytes on the transport, not fidelity, so a lossy `jpeg` is\n * roughly an order of magnitude smaller per frame and is what makes a live\n * view feel like a remote desktop rather than a slideshow.\n */\nexport type DeskFrameFormat = 'png' | 'jpeg'\n\n/**\n * The two kinds of unit a video stream is made of.\n *\n * `init` is the header a decoder needs before it can make sense of anything —\n * for fragmented MP4, `ftyp` plus `moov`. It arrives once per encoder start and\n * a consumer that never receives it decodes nothing at all, silently. `media`\n * is one fragment of pictures. Ordering is load-bearing: init first, then media\n * from a keyframe onwards.\n */\nexport type DeskVideoChunkKind = 'init' | 'media'\n\nexport type DeskHandoverScope = 'view' | 'control'\n\nexport type DeskHandoverEndReason = 'ended' | 'expired' | 'revoked'\n\n/**\n * The closed union of recordable desk activity. The consumer's ledger stores\n * these rows; the union is defined here so the mechanism and the record agree\n * on the taxonomy without the package ever touching a database.\n */\nexport type DeskEventDetail =\n | {\n kind: 'shell_command'\n command: string\n args: readonly string[]\n cwd: string | null\n exitCode: number | null\n signal: string | null\n }\n | { kind: 'app_launch'; appId: string; args: readonly string[] }\n | { kind: 'click'; x: number; y: number; button: DeskPointerButton }\n | { kind: 'type'; text: string }\n | { kind: 'key'; combo: string }\n | { kind: 'scroll'; x: number; y: number; dx: number; dy: number }\n | { kind: 'drag'; from: DeskPoint; to: DeskPoint }\n | { kind: 'window_focus'; window: WindowInfo }\n | { kind: 'screen_open'; width: number; height: number }\n | { kind: 'screen_close' }\n | { kind: 'handover_begin'; actor: string | null; scope: DeskHandoverScope }\n | { kind: 'handover_end'; actor: string | null; durationMs: number }\n | { kind: 'job_start'; jobId: string; command: string; args: readonly string[] }\n | { kind: 'job_exit'; jobId: string; exitCode: number | null; signal: string | null }\n\nexport type DeskEventKind = DeskEventDetail['kind']\n\nexport type DeskEvent = DeskEventDetail & { deskId: string; at: string }\n\n/**\n * Handover boundaries as the audit port sees them: who took over, under what\n * scope, and for how long — never what they typed.\n */\nexport type DeskAuditEntry =\n | {\n kind: 'handover_begin'\n deskId: string\n actor: string | null\n scope: DeskHandoverScope\n ttlMs: number\n at: string\n }\n | {\n kind: 'handover_end'\n deskId: string\n actor: string | null\n durationMs: number\n reason: DeskHandoverEndReason\n at: string\n }\n\n/**\n * Consumer-supplied policy. Absent hooks allow: the package is mechanism, and\n * a deployment that wants governance supplies it here. A hook returning\n * `false` makes the corresponding call throw before anything reaches the\n * guest.\n */\nexport interface DeskPolicyPort {\n allowExec?(request: {\n deskId: string\n command: string\n args: readonly string[]\n keepAlive: boolean\n }): boolean\n allowScreen?(request: { deskId: string; width: number; height: number }): boolean\n allowHandover?(request: {\n deskId: string\n scope: DeskHandoverScope\n ttlMs: number\n }): boolean\n}\n\n/**\n * The ports a consumer supplies to `createDeskHost`.\n *\n * Masking contract (load-bearing): while a handover is active, input events —\n * `click`, `type`, `key`, `scroll`, `drag` — and the screen-derived events\n * `window_focus` and `app_launch` never reach `onEvent`, and frames are never\n * emitted to `frames()` consumers. Only `handover_begin` and `handover_end`\n * cross the boundary, carrying actor and duration. Getting this wrong leaks\n * credentials into an append-only record.\n */\nexport interface DeskPorts {\n policy?: DeskPolicyPort\n onEvent: (event: DeskEvent) => void\n audit: (entry: DeskAuditEntry) => void\n}\n"]}
package/frames.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ export type DeskFrameIdentity = {
2
+ frameId: string;
3
+ changed: boolean;
4
+ };
5
+ export interface DeskFrameDeduplicator {
6
+ observe(data: Uint8Array | string, mediaType?: string): DeskFrameIdentity;
7
+ reset(): void;
8
+ }
9
+ /**
10
+ * Exact frame identity for model and recording pipelines. The digest is safe
11
+ * to persist and compare; pixel bytes need not be retained after hashing.
12
+ */
13
+ export declare function createDeskFrameDeduplicator(): DeskFrameDeduplicator;
14
+ //# sourceMappingURL=frames.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frames.d.ts","sourceRoot":"","sources":["../src/frames.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAAA;IACzE,KAAK,IAAI,IAAI,CAAA;CACd;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,IAAI,qBAAqB,CAiBnE"}
package/frames.js ADDED
@@ -0,0 +1,24 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * Exact frame identity for model and recording pipelines. The digest is safe
4
+ * to persist and compare; pixel bytes need not be retained after hashing.
5
+ */
6
+ export function createDeskFrameDeduplicator() {
7
+ let previous = null;
8
+ return {
9
+ observe(data, mediaType = 'image/png') {
10
+ const hash = createHash('sha256');
11
+ hash.update(mediaType);
12
+ hash.update('\0');
13
+ hash.update(typeof data === 'string' ? Buffer.from(data, 'base64') : data);
14
+ const frameId = `sha256:${hash.digest('hex')}`;
15
+ const changed = frameId !== previous;
16
+ previous = frameId;
17
+ return { frameId, changed };
18
+ },
19
+ reset() {
20
+ previous = null;
21
+ },
22
+ };
23
+ }
24
+ //# sourceMappingURL=frames.js.map
package/frames.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frames.js","sourceRoot":"","sources":["../src/frames.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAYxC;;;GAGG;AACH,MAAM,UAAU,2BAA2B;IACzC,IAAI,QAAQ,GAAkB,IAAI,CAAA;IAClC,OAAO;QACL,OAAO,CAAC,IAAI,EAAE,SAAS,GAAG,WAAW;YACnC,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAA;YACjC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;YACtB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;YACjB,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;YAC1E,MAAM,OAAO,GAAG,UAAU,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAA;YAC9C,MAAM,OAAO,GAAG,OAAO,KAAK,QAAQ,CAAA;YACpC,QAAQ,GAAG,OAAO,CAAA;YAClB,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAA;QAC7B,CAAC;QACD,KAAK;YACH,QAAQ,GAAG,IAAI,CAAA;QACjB,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["import { createHash } from 'node:crypto'\n\nexport type DeskFrameIdentity = {\n frameId: string\n changed: boolean\n}\n\nexport interface DeskFrameDeduplicator {\n observe(data: Uint8Array | string, mediaType?: string): DeskFrameIdentity\n reset(): void\n}\n\n/**\n * Exact frame identity for model and recording pipelines. The digest is safe\n * to persist and compare; pixel bytes need not be retained after hashing.\n */\nexport function createDeskFrameDeduplicator(): DeskFrameDeduplicator {\n let previous: string | null = null\n return {\n observe(data, mediaType = 'image/png') {\n const hash = createHash('sha256')\n hash.update(mediaType)\n hash.update('\\0')\n hash.update(typeof data === 'string' ? Buffer.from(data, 'base64') : data)\n const frameId = `sha256:${hash.digest('hex')}`\n const changed = frameId !== previous\n previous = frameId\n return { frameId, changed }\n },\n reset() {\n previous = null\n },\n }\n}\n"]}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The in-guest agent: PID 1's helper inside the microVM, speaking the framed
3
+ * JSON protocol over a vsock stream. This is the only new attack surface in
4
+ * the desk design, so it is deliberately small: a bounded frame decoder, a
5
+ * strict parser, a closed dispatch switch, and nothing else. Everything that
6
+ * touches the guest OS — spawning processes, driving the compositor, reading
7
+ * the clipboard — is injected as a handler so the message-handling core stays
8
+ * pure and unit-testable on any platform.
9
+ *
10
+ * The module depends only on `node:` built-ins and this package's own
11
+ * protocol module; it ships in the same package and is started by the guest's
12
+ * init.
13
+ */
14
+ import { Buffer } from 'node:buffer';
15
+ import type { Duplex } from 'node:stream';
16
+ import { type GuestEventMessage, type GuestExecResult, type GuestObservation, type GuestInput } from './protocol.js';
17
+ import type { DeskFrameFormat, DeskHandoverScope } from './events.js';
18
+ export interface GuestExecCall {
19
+ command: string;
20
+ args: readonly string[];
21
+ cwd: string | null;
22
+ env: Readonly<Record<string, string>> | null;
23
+ timeoutMs: number | null;
24
+ }
25
+ export interface GuestJobStartCall {
26
+ command: string;
27
+ args: readonly string[];
28
+ cwd: string | null;
29
+ env: Readonly<Record<string, string>> | null;
30
+ }
31
+ /**
32
+ * The operations the guest environment implements. Handlers own all contact
33
+ * with the OS; the core owns all contact with the wire.
34
+ */
35
+ export interface GuestAgentHandlers {
36
+ exec(call: GuestExecCall): Promise<GuestExecResult>;
37
+ jobStart(call: GuestJobStartCall): Promise<{
38
+ jobId: string;
39
+ }>;
40
+ jobSignal(call: {
41
+ jobId: string;
42
+ signal: 'SIGTERM' | 'SIGKILL';
43
+ }): Promise<void>;
44
+ screenStart(call: {
45
+ width: number;
46
+ height: number;
47
+ }): Promise<void>;
48
+ screenStop(): Promise<void>;
49
+ observe(): Promise<GuestObservation>;
50
+ input(action: GuestInput): Promise<void>;
51
+ a11yInvoke(call: {
52
+ nodeId: string;
53
+ action: string;
54
+ }): Promise<void>;
55
+ launch(call: {
56
+ appId: string;
57
+ args: readonly string[];
58
+ }): Promise<void>;
59
+ clipboardRead(): Promise<{
60
+ text: string;
61
+ }>;
62
+ clipboardWrite(call: {
63
+ text: string;
64
+ }): Promise<void>;
65
+ /**
66
+ * `format` absent means the host did not ask, so the guest picks — an older
67
+ * host never sends it and must still get a stream it can decode.
68
+ */
69
+ framesStart(call: {
70
+ fps: number;
71
+ width: number;
72
+ height: number;
73
+ format?: DeskFrameFormat;
74
+ }): Promise<void>;
75
+ framesStop(): Promise<void>;
76
+ /**
77
+ * Start encoding the screen as video. Separate from `framesStart` because it
78
+ * is a different job with a different transport shape: one long-lived encoder
79
+ * whose output only means anything in order, rather than independent pictures.
80
+ */
81
+ videoStart(call: {
82
+ fps: number;
83
+ width: number;
84
+ height: number;
85
+ }): Promise<void>;
86
+ videoStop(): Promise<void>;
87
+ handoverBegin(call: {
88
+ ttlMs: number;
89
+ scope: DeskHandoverScope;
90
+ }): Promise<{
91
+ url: string;
92
+ }>;
93
+ handoverEnd(): Promise<void>;
94
+ capabilities(): Promise<{
95
+ virtioGpu: boolean;
96
+ }>;
97
+ }
98
+ export interface GuestAgentCore {
99
+ /**
100
+ * Consume one chunk of the byte stream and return the frames to write back,
101
+ * in order. Malformed requests produce error responses; a framing violation
102
+ * (oversized or non-JSON frame) throws, and the caller must drop the
103
+ * connection — a length-prefixed stream cannot be resynchronized once its
104
+ * lengths are untrusted.
105
+ */
106
+ handleChunk(chunk: Buffer): Promise<Buffer[]>;
107
+ /** Encode an unsolicited guest-to-host event frame. */
108
+ encodeEvent(event: GuestEventMessage): Buffer;
109
+ }
110
+ export declare function createGuestAgentCore(handlers: GuestAgentHandlers, options?: {
111
+ maxFrameBytes?: number;
112
+ }): GuestAgentCore;
113
+ export interface RunningGuestAgent {
114
+ /** Push an unsolicited event (job exit, frame, focus change) to the host. */
115
+ sendEvent(event: GuestEventMessage): void;
116
+ /** Resolves when the connection has closed. */
117
+ closed: Promise<void>;
118
+ }
119
+ /**
120
+ * Bind the pure core to a real byte stream. This is the entire impure surface
121
+ * of the guest agent: read chunks, write responses, destroy the connection on
122
+ * a framing violation.
123
+ */
124
+ export declare function runGuestAgent(options: {
125
+ stream: Duplex;
126
+ handlers: GuestAgentHandlers;
127
+ maxFrameBytes?: number;
128
+ }): RunningGuestAgent;
129
+ //# sourceMappingURL=guest-agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guest-agent.d.ts","sourceRoot":"","sources":["../src/guest-agent.ts"],"names":[],"mappings":"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"}