Humuhumu33 commited on
Commit
b05742c
Β·
verified Β·
1 Parent(s): 65a163d

Evict apps/holo-linux to kappa-store (M3): 27 blake3 objects

Browse files
Files changed (28) hide show
  1. .gitattributes +2 -0
  2. b/00b5326a4545594a940e078d74bec9064a36162a04eb3fd2a90fe1a4af14d841 +3 -0
  3. b/0be65ad91c9fc0c898229b532748d569154398412d8737ff5e9d13f5d6b92a99 +2 -0
  4. b/1fb7d44bf66d1ba1e77065e78cbe4666b95f7daa8e714117c98507072ddc1f4c +2 -0
  5. b/2b0ef5fab86c2f515e457cd0238e958f40021db7c7d84d3dad896cad7ca83ea7 +498 -0
  6. b/3cd10cb95409eb044bdc15bbfeae1d380d98e4cb6b5635e3d7e5979d30eba593 +17 -0
  7. b/456ac33710c2dee959da5703727d01fbf5350a51ac24077488ca1a9a0332e08f +2 -0
  8. b/481ecdef6a6186997d3771adbe807393ed61d8af62e4897f5cf9314abc447c50 +0 -0
  9. b/4c3cfb4598607c4de31a9a6bfe1b2de3ecac4a3cc9d347449913ae3c22299fe6 +26 -0
  10. b/69e9bfc55ac61137f83090028126b4484b19bd3075a74e9cd0c77aaf1e489bc3 +329 -0
  11. b/78381834c93186218a1c3be259d514cfbf151e40b99b781ea9feb79e371a99c5 +21 -0
  12. b/78ec7022aff858c52db5fda0a79a03b757d9d2b186e3c25ce3357ada0319985d +16 -0
  13. b/816d2f31ed1feb6e4d18e69c6bd81cdf9d1cc63fd650680b9f4ed83867cd52c6 +696 -0
  14. b/86444b599981098edbc377ed17cf81236efd56321edb86119dcf516f80fc2632 +3 -0
  15. b/95866a33a0ac86be3a1c8b454dd23b1c6279133ff195dd8d73c3ca98cea1a31c +8 -0
  16. b/95d704019ecda0ed07685277867299b1e7cc94c0f2e435c9f047bd5cdeb5cdab +79 -0
  17. b/996b3aa91392ca5e20a66924127be48eb5f804ad1ca48525f9363ff010307427 +26 -0
  18. b/a0cf7e001e6b509301f709a952fe2f5073b95ba618dd991874ef26aa85d8891d +33 -0
  19. b/ac97ac99cb90425d4fe91bcc20c7149e043a2574f62999c79d28ca8515229376 +0 -0
  20. b/b287e4c57be0b52c3182bf43edc16bda29a1dcdf0d5ec2d486e1944f8f13d654 +2 -0
  21. b/b2bb781c8c15f4717080e7bde1195d79eb1363dd734633c630f4c587670eb884 +27 -0
  22. b/c2fbf3e19ed81c6b5ad45c60b95289e5b483789861f7bd277ca99bd85897047f +218 -0
  23. b/c50bbb654dc8ebf24e9ef81a8d187a66de48a1cff620cf54fb2ff8f74832f799 +21 -0
  24. b/cbbef78890e83aa015dca82a2d6d170e5865de80c57fd45dcd48535e0d90d0de +2 -0
  25. b/cdd0afa46a7402609a1dd436aa41cafeb01615340e25423a2af8e9ddc7c4026b +62 -0
  26. b/dcbe5d1951a6b28a0e69ac955073ba55891275b754ddbb232cf12bfbf9bf1cde +5 -0
  27. b/dd131ed675da2a674042c42fc01402d48a323da675371368e89a4342cb02ccb1 +147 -0
  28. b/ffab5e5a4c043c5a5e4d1391583eea58a46c6f0e4141a9575e1c407a4ed25bf1 +70 -0
.gitattributes CHANGED
@@ -63,3 +63,5 @@ b/cd4d521be3e2bdbc82e815d8411b0e92c31f935584d4bb5f4b9ec911434f90de filter=lfs di
63
  b/d63c7e2117883cf9ec8551a013d48695c4fc8183c81c1c704a1920060fcfe570 filter=lfs diff=lfs merge=lfs -text
64
  b/ed23baff6c0015f9a92afba85a8e1043afe1ca57733c08ad8ff79273270ef861 filter=lfs diff=lfs merge=lfs -text
65
  b/535ed9bd32c3c964ae2cd9dbf69406fc007db53501a6bdbefe33c70d949e4b90 filter=lfs diff=lfs merge=lfs -text
 
 
 
63
  b/d63c7e2117883cf9ec8551a013d48695c4fc8183c81c1c704a1920060fcfe570 filter=lfs diff=lfs merge=lfs -text
64
  b/ed23baff6c0015f9a92afba85a8e1043afe1ca57733c08ad8ff79273270ef861 filter=lfs diff=lfs merge=lfs -text
65
  b/535ed9bd32c3c964ae2cd9dbf69406fc007db53501a6bdbefe33c70d949e4b90 filter=lfs diff=lfs merge=lfs -text
66
+ b/00b5326a4545594a940e078d74bec9064a36162a04eb3fd2a90fe1a4af14d841 filter=lfs diff=lfs merge=lfs -text
67
+ b/86444b599981098edbc377ed17cf81236efd56321edb86119dcf516f80fc2632 filter=lfs diff=lfs merge=lfs -text
b/00b5326a4545594a940e078d74bec9064a36162a04eb3fd2a90fe1a4af14d841 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8f95f671a596bcbd88e115f388c71df95d2552fb558aed4bebe0234eacca2c0d
3
+ size 28344286
b/0be65ad91c9fc0c898229b532748d569154398412d8737ff5e9d13f5d6b92a99 ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()));
2
+ //# sourceMappingURL=addon-fit.js.map
b/1fb7d44bf66d1ba1e77065e78cbe4666b95f7daa8e714117c98507072ddc1f4c ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Unicode11Addon=t():e.Unicode11Addon=t()}(this,(()=>(()=>{"use strict";var e={433:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeV11=void 0;const r=i(938),s=[[768,879],[1155,1161],[1425,1469],[1471,1471],[1473,1474],[1476,1477],[1479,1479],[1536,1541],[1552,1562],[1564,1564],[1611,1631],[1648,1648],[1750,1757],[1759,1764],[1767,1768],[1770,1773],[1807,1807],[1809,1809],[1840,1866],[1958,1968],[2027,2035],[2045,2045],[2070,2073],[2075,2083],[2085,2087],[2089,2093],[2137,2139],[2259,2306],[2362,2362],[2364,2364],[2369,2376],[2381,2381],[2385,2391],[2402,2403],[2433,2433],[2492,2492],[2497,2500],[2509,2509],[2530,2531],[2558,2558],[2561,2562],[2620,2620],[2625,2626],[2631,2632],[2635,2637],[2641,2641],[2672,2673],[2677,2677],[2689,2690],[2748,2748],[2753,2757],[2759,2760],[2765,2765],[2786,2787],[2810,2815],[2817,2817],[2876,2876],[2879,2879],[2881,2884],[2893,2893],[2902,2902],[2914,2915],[2946,2946],[3008,3008],[3021,3021],[3072,3072],[3076,3076],[3134,3136],[3142,3144],[3146,3149],[3157,3158],[3170,3171],[3201,3201],[3260,3260],[3263,3263],[3270,3270],[3276,3277],[3298,3299],[3328,3329],[3387,3388],[3393,3396],[3405,3405],[3426,3427],[3530,3530],[3538,3540],[3542,3542],[3633,3633],[3636,3642],[3655,3662],[3761,3761],[3764,3772],[3784,3789],[3864,3865],[3893,3893],[3895,3895],[3897,3897],[3953,3966],[3968,3972],[3974,3975],[3981,3991],[3993,4028],[4038,4038],[4141,4144],[4146,4151],[4153,4154],[4157,4158],[4184,4185],[4190,4192],[4209,4212],[4226,4226],[4229,4230],[4237,4237],[4253,4253],[4448,4607],[4957,4959],[5906,5908],[5938,5940],[5970,5971],[6002,6003],[6068,6069],[6071,6077],[6086,6086],[6089,6099],[6109,6109],[6155,6158],[6277,6278],[6313,6313],[6432,6434],[6439,6440],[6450,6450],[6457,6459],[6679,6680],[6683,6683],[6742,6742],[6744,6750],[6752,6752],[6754,6754],[6757,6764],[6771,6780],[6783,6783],[6832,6846],[6912,6915],[6964,6964],[6966,6970],[6972,6972],[6978,6978],[7019,7027],[7040,7041],[7074,7077],[7080,7081],[7083,7085],[7142,7142],[7144,7145],[7149,7149],[7151,7153],[7212,7219],[7222,7223],[7376,7378],[7380,7392],[7394,7400],[7405,7405],[7412,7412],[7416,7417],[7616,7673],[7675,7679],[8203,8207],[8234,8238],[8288,8292],[8294,8303],[8400,8432],[11503,11505],[11647,11647],[11744,11775],[12330,12333],[12441,12442],[42607,42610],[42612,42621],[42654,42655],[42736,42737],[43010,43010],[43014,43014],[43019,43019],[43045,43046],[43204,43205],[43232,43249],[43263,43263],[43302,43309],[43335,43345],[43392,43394],[43443,43443],[43446,43449],[43452,43453],[43493,43493],[43561,43566],[43569,43570],[43573,43574],[43587,43587],[43596,43596],[43644,43644],[43696,43696],[43698,43700],[43703,43704],[43710,43711],[43713,43713],[43756,43757],[43766,43766],[44005,44005],[44008,44008],[44013,44013],[64286,64286],[65024,65039],[65056,65071],[65279,65279],[65529,65531]],n=[[66045,66045],[66272,66272],[66422,66426],[68097,68099],[68101,68102],[68108,68111],[68152,68154],[68159,68159],[68325,68326],[68900,68903],[69446,69456],[69633,69633],[69688,69702],[69759,69761],[69811,69814],[69817,69818],[69821,69821],[69837,69837],[69888,69890],[69927,69931],[69933,69940],[70003,70003],[70016,70017],[70070,70078],[70089,70092],[70191,70193],[70196,70196],[70198,70199],[70206,70206],[70367,70367],[70371,70378],[70400,70401],[70459,70460],[70464,70464],[70502,70508],[70512,70516],[70712,70719],[70722,70724],[70726,70726],[70750,70750],[70835,70840],[70842,70842],[70847,70848],[70850,70851],[71090,71093],[71100,71101],[71103,71104],[71132,71133],[71219,71226],[71229,71229],[71231,71232],[71339,71339],[71341,71341],[71344,71349],[71351,71351],[71453,71455],[71458,71461],[71463,71467],[71727,71735],[71737,71738],[72148,72151],[72154,72155],[72160,72160],[72193,72202],[72243,72248],[72251,72254],[72263,72263],[72273,72278],[72281,72283],[72330,72342],[72344,72345],[72752,72758],[72760,72765],[72767,72767],[72850,72871],[72874,72880],[72882,72883],[72885,72886],[73009,73014],[73018,73018],[73020,73021],[73023,73029],[73031,73031],[73104,73105],[73109,73109],[73111,73111],[73459,73460],[78896,78904],[92912,92916],[92976,92982],[94031,94031],[94095,94098],[113821,113822],[113824,113827],[119143,119145],[119155,119170],[119173,119179],[119210,119213],[119362,119364],[121344,121398],[121403,121452],[121461,121461],[121476,121476],[121499,121503],[121505,121519],[122880,122886],[122888,122904],[122907,122913],[122915,122916],[122918,122922],[123184,123190],[123628,123631],[125136,125142],[125252,125258],[917505,917505],[917536,917631],[917760,917999]],o=[[4352,4447],[8986,8987],[9001,9002],[9193,9196],[9200,9200],[9203,9203],[9725,9726],[9748,9749],[9800,9811],[9855,9855],[9875,9875],[9889,9889],[9898,9899],[9917,9918],[9924,9925],[9934,9934],[9940,9940],[9962,9962],[9970,9971],[9973,9973],[9978,9978],[9981,9981],[9989,9989],[9994,9995],[10024,10024],[10060,10060],[10062,10062],[10067,10069],[10071,10071],[10133,10135],[10160,10160],[10175,10175],[11035,11036],[11088,11088],[11093,11093],[11904,11929],[11931,12019],[12032,12245],[12272,12283],[12288,12329],[12334,12350],[12353,12438],[12443,12543],[12549,12591],[12593,12686],[12688,12730],[12736,12771],[12784,12830],[12832,12871],[12880,19903],[19968,42124],[42128,42182],[43360,43388],[44032,55203],[63744,64255],[65040,65049],[65072,65106],[65108,65126],[65128,65131],[65281,65376],[65504,65510]],c=[[94176,94179],[94208,100343],[100352,101106],[110592,110878],[110928,110930],[110948,110951],[110960,111355],[126980,126980],[127183,127183],[127374,127374],[127377,127386],[127488,127490],[127504,127547],[127552,127560],[127568,127569],[127584,127589],[127744,127776],[127789,127797],[127799,127868],[127870,127891],[127904,127946],[127951,127955],[127968,127984],[127988,127988],[127992,128062],[128064,128064],[128066,128252],[128255,128317],[128331,128334],[128336,128359],[128378,128378],[128405,128406],[128420,128420],[128507,128591],[128640,128709],[128716,128716],[128720,128722],[128725,128725],[128747,128748],[128756,128762],[128992,129003],[129293,129393],[129395,129398],[129402,129442],[129445,129450],[129454,129482],[129485,129535],[129648,129651],[129656,129658],[129664,129666],[129680,129685],[131072,196605],[196608,262141]];let l;function d(e,t){let i,r=0,s=t.length-1;if(e<t[0][0]||e>t[s][1])return!1;for(;s>=r;)if(i=r+s>>1,e>t[i][1])r=i+1;else{if(!(e<t[i][0]))return!0;s=i-1}return!1}t.UnicodeV11=class{constructor(){if(this.version="11",!l){l=new Uint8Array(65536),l.fill(1),l[0]=0,l.fill(0,1,32),l.fill(0,127,160);for(let e=0;e<s.length;++e)l.fill(0,s[e][0],s[e][1]+1);for(let e=0;e<o.length;++e)l.fill(2,o[e][0],o[e][1]+1)}}wcwidth(e){return e<32?0:e<127?1:e<65536?l[e]:d(e,n)?0:d(e,c)?2:1}charProperties(e,t){let i=this.wcwidth(e),s=0===i&&0!==t;if(s){const e=r.UnicodeService.extractWidth(t);0===e?s=!1:e>i&&(i=e)}return r.UnicodeService.createPropertyValue(0,i,s)}}},345:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.runAndSubscribe=t.forwardEvent=t.EventEmitter=void 0,t.EventEmitter=class{constructor(){this._listeners=[],this._disposed=!1}get event(){return this._event||(this._event=e=>(this._listeners.push(e),{dispose:()=>{if(!this._disposed)for(let t=0;t<this._listeners.length;t++)if(this._listeners[t]===e)return void this._listeners.splice(t,1)}})),this._event}fire(e,t){const i=[];for(let e=0;e<this._listeners.length;e++)i.push(this._listeners[e]);for(let r=0;r<i.length;r++)i[r].call(void 0,e,t)}dispose(){this.clearListeners(),this._disposed=!0}clearListeners(){this._listeners&&(this._listeners.length=0)}},t.forwardEvent=function(e,t){return e((e=>t.fire(e)))},t.runAndSubscribe=function(e,t){return t(void 0),e((e=>t(e)))}},490:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeV6=void 0;const r=i(938),s=[[768,879],[1155,1158],[1160,1161],[1425,1469],[1471,1471],[1473,1474],[1476,1477],[1479,1479],[1536,1539],[1552,1557],[1611,1630],[1648,1648],[1750,1764],[1767,1768],[1770,1773],[1807,1807],[1809,1809],[1840,1866],[1958,1968],[2027,2035],[2305,2306],[2364,2364],[2369,2376],[2381,2381],[2385,2388],[2402,2403],[2433,2433],[2492,2492],[2497,2500],[2509,2509],[2530,2531],[2561,2562],[2620,2620],[2625,2626],[2631,2632],[2635,2637],[2672,2673],[2689,2690],[2748,2748],[2753,2757],[2759,2760],[2765,2765],[2786,2787],[2817,2817],[2876,2876],[2879,2879],[2881,2883],[2893,2893],[2902,2902],[2946,2946],[3008,3008],[3021,3021],[3134,3136],[3142,3144],[3146,3149],[3157,3158],[3260,3260],[3263,3263],[3270,3270],[3276,3277],[3298,3299],[3393,3395],[3405,3405],[3530,3530],[3538,3540],[3542,3542],[3633,3633],[3636,3642],[3655,3662],[3761,3761],[3764,3769],[3771,3772],[3784,3789],[3864,3865],[3893,3893],[3895,3895],[3897,3897],[3953,3966],[3968,3972],[3974,3975],[3984,3991],[3993,4028],[4038,4038],[4141,4144],[4146,4146],[4150,4151],[4153,4153],[4184,4185],[4448,4607],[4959,4959],[5906,5908],[5938,5940],[5970,5971],[6002,6003],[6068,6069],[6071,6077],[6086,6086],[6089,6099],[6109,6109],[6155,6157],[6313,6313],[6432,6434],[6439,6440],[6450,6450],[6457,6459],[6679,6680],[6912,6915],[6964,6964],[6966,6970],[6972,6972],[6978,6978],[7019,7027],[7616,7626],[7678,7679],[8203,8207],[8234,8238],[8288,8291],[8298,8303],[8400,8431],[12330,12335],[12441,12442],[43014,43014],[43019,43019],[43045,43046],[64286,64286],[65024,65039],[65056,65059],[65279,65279],[65529,65531]],n=[[68097,68099],[68101,68102],[68108,68111],[68152,68154],[68159,68159],[119143,119145],[119155,119170],[119173,119179],[119210,119213],[119362,119364],[917505,917505],[917536,917631],[917760,917999]];let o;t.UnicodeV6=class{constructor(){if(this.version="6",!o){o=new Uint8Array(65536),o.fill(1),o[0]=0,o.fill(0,1,32),o.fill(0,127,160),o.fill(2,4352,4448),o[9001]=2,o[9002]=2,o.fill(2,11904,42192),o[12351]=1,o.fill(2,44032,55204),o.fill(2,63744,64256),o.fill(2,65040,65050),o.fill(2,65072,65136),o.fill(2,65280,65377),o.fill(2,65504,65511);for(let e=0;e<s.length;++e)o.fill(0,s[e][0],s[e][1]+1)}}wcwidth(e){return e<32?0:e<127?1:e<65536?o[e]:function(e,t){let i,r=0,s=t.length-1;if(e<t[0][0]||e>t[s][1])return!1;for(;s>=r;)if(i=r+s>>1,e>t[i][1])r=i+1;else{if(!(e<t[i][0]))return!0;s=i-1}return!1}(e,n)?0:e>=131072&&e<=196605||e>=196608&&e<=262141?2:1}charProperties(e,t){let i=this.wcwidth(e),s=0===i&&0!==t;if(s){const e=r.UnicodeService.extractWidth(t);0===e?s=!1:e>i&&(i=e)}return r.UnicodeService.createPropertyValue(0,i,s)}}},938:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeService=void 0;const r=i(345),s=i(490);class n{static extractShouldJoin(e){return 0!=(1&e)}static extractWidth(e){return e>>1&3}static extractCharKind(e){return e>>3}static createPropertyValue(e,t,i=!1){return(16777215&e)<<3|(3&t)<<1|(i?1:0)}constructor(){this._providers=Object.create(null),this._active="",this._onChange=new r.EventEmitter,this.onChange=this._onChange.event;const e=new s.UnicodeV6;this.register(e),this._active=e.version,this._activeProvider=e}dispose(){this._onChange.dispose()}get versions(){return Object.keys(this._providers)}get activeVersion(){return this._active}set activeVersion(e){if(!this._providers[e])throw new Error(`unknown Unicode version "${e}"`);this._active=e,this._activeProvider=this._providers[e],this._onChange.fire(e)}register(e){this._providers[e.version]=e}wcwidth(e){return this._activeProvider.wcwidth(e)}getStringCellWidth(e){let t=0,i=0;const r=e.length;for(let s=0;s<r;++s){let o=e.charCodeAt(s);if(55296<=o&&o<=56319){if(++s>=r)return t+this.wcwidth(o);const i=e.charCodeAt(s);56320<=i&&i<=57343?o=1024*(o-55296)+i-56320+65536:t+=this.wcwidth(i)}const c=this.charProperties(o,i);let l=n.extractWidth(c);n.extractShouldJoin(c)&&(l-=n.extractWidth(i)),t+=l,i=c}return t}charProperties(e,t){return this._activeProvider.charProperties(e,t)}}t.UnicodeService=n}},t={};function i(r){var s=t[r];if(void 0!==s)return s.exports;var n=t[r]={exports:{}};return e[r](n,n.exports,i),n.exports}var r={};return(()=>{var e=r;Object.defineProperty(e,"__esModule",{value:!0}),e.Unicode11Addon=void 0;const t=i(433);e.Unicode11Addon=class{activate(e){e.unicode.register(new t.UnicodeV11)}dispose(){}}})(),r})()));
2
+ //# sourceMappingURL=addon-unicode11.js.map
b/2b0ef5fab86c2f515e457cd0238e958f40021db7c7d84d3dad896cad7ca83ea7 ADDED
@@ -0,0 +1,498 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // holo-linux-worker.js β€” the Holo Linux boot worker.
2
+ //
3
+ // Boots a REAL riscv64 Linux 6.6 kernel on a real Debian 13 (trixie) rootfs, off
4
+ // the main thread, on the holospaces in-browser ISA core (the same engine the OS
5
+ // holospace uses β€” crates/holospaces-web, compiled to wasm). The holospace IS the
6
+ // machine: no server boots the kernel (Law L1/L4).
7
+ //
8
+ // What this worker adds over the plain OS path is a HARD ΞΊ-GATE (Law L5): before a
9
+ // single guest instruction runs, it re-derives the SHA-256 of the fetched kernel +
10
+ // rootfs and refuses to boot unless each matches its content address in kappa.json.
11
+ // The kernel's ΞΊ is the SAME content address pinned canonically at /boot/kernel.uor.json
12
+ // β€” so "anchored in the k-addressable substrate" is proven at load, not asserted.
13
+ //
14
+ // The console is the holospace's terminal channel: the guest's own serial tty is
15
+ // streamed to xterm.js via `terminal_delta()` and keystrokes go back through
16
+ // `feed_input()` β€” a real tty, so the guest's line discipline echoes, edits and
17
+ // raises SIGINT on Ctrl-C. Real PIDs, real syscalls hitting a real kernel.
18
+ //
19
+ // Protocol (main <-> worker):
20
+ // main -> worker: {type:'stdin', data:Uint8Array} raw keystrokes
21
+ // worker -> main: {type:'status', text} boot progress
22
+ // {type:'kappa', which, name, expected, actual, ok, canonical}
23
+ // {type:'booted'} machine running
24
+ // {type:'stdout', data:Uint8Array} console delta
25
+ // {type:'mips', mips} live throughput
26
+ // {type:'halt', reason} guest powered off
27
+ // {type:'error', text, hint?} fatal (gate failure included)
28
+
29
+ // The wasm engine is served from the OS /pkg seam (absolute): natively the ΞΊ-route serves holo://os/pkg
30
+ // (make-dist projects usr/lib/pkg β†’ /pkg); the dev server maps /pkg the same way. (Was ./pkg, which only
31
+ // existed under the dev serve and was never staged into the native image.)
32
+ import init, { DevcontainerImage, Workspace } from "/pkg/holospaces_web.js?v=hl1";
33
+
34
+ const Q = new URLSearchParams(self.location.search);
35
+ // Per-TAB session id: OPFS hands the ΞΊ-disk file to one tab at a time, so each tab
36
+ // namespaces its own writable disk β†’ multiple tabs each run an INDEPENDENT machine.
37
+ const SID = (Q.get("sid") || "default").replace(/[^a-z0-9]/gi, "").slice(0, 16) || "default";
38
+ // Writable ext4 the OS works in. The ΞΊ-disk is sparse + disk-backed (OPFS), so boot
39
+ // time and wasm-heap cost are independent of this size. 1 GiB default; cap < 4 GiB
40
+ // (a 32-bit wasm byte length would wrap). Tunable via ?disk=<MiB>.
41
+ const DISK_MIB = Math.max(16, Math.min(3072, parseInt(Q.get("disk") || "1024", 10) || 1024));
42
+ const DISK_BYTES = DISK_MIB * 1024 * 1024;
43
+
44
+ const KERNEL_URL = "./os-kernel.gz";
45
+ const ROOTFS_URL = "./os-rootfs.tar.gz";
46
+ const PINS_URL = "./kappa.json";
47
+ const OPFS_ROOTFS = `hl-rootfs-${SID}.ext4`;
48
+ const OPFS_DISK_PACK = `hl-disk-${SID}.kpack`;
49
+ const LAYER_MEDIA = "application/vnd.oci.image.layer.v1.tar+gzip";
50
+
51
+ // ── Local persistence (CC-30) ──────────────────────────────────────────────────
52
+ // The machine you build in survives a full browser restart. The engine's `suspend()`
53
+ // captures the whole machine β€” CPU + RAM + the rootfs disk + the workspace files β€” as
54
+ // canonical, content-addressed bytes; we gzip them into OPFS, and the next launch
55
+ // `resume_devcontainer()`s them instead of cold-booting. The snapshot is keyed by the
56
+ // (stable) machine id so it reattaches across restarts. Persistent local storage AND a
57
+ // persistent, content-addressed machine identity (ΞΊ of the snapshot) fall out of this.
58
+ const OPFS_STATE = `hl-state-${SID}.snap.gz`; // legacy plaintext snapshot (pre-seal) β€” cleaned up on boot
59
+ const OPFS_STATE_META = `hl-state-${SID}.json`; // legacy meta
60
+ // Sovereign machine (sealed-at-rest): the snapshot is AES-256-GCM ciphertext on disk, owner-signed
61
+ // (ECDSA P-256) and bound to a NON-EXTRACTABLE, device-local key β€” so a copied blob cannot be opened or
62
+ // forged on another device, and there is no readable machine at rest. (TEE/biometric gate = next milestone.)
63
+ const OPFS_SEALED = `hl-machine-${SID}.sealed`; // iv(12) || AES-GCM ciphertext(gzip(snapshot))
64
+ const OPFS_SEALED_META = `hl-machine-${SID}.json`; // {kappa, sealedKappa, sig, pub, alg, attestRoot, kernelKappa, …}
65
+ const EPHEMERAL = Q.has("ephemeral"); // never persist, never resume (throwaway machine)
66
+ const RESET = Q.has("fresh"); // discard any saved machine, cold-boot, then persist anew
67
+ // Periodic crash-resilient autosave while running (ms; 0 disables). Lifecycle events
68
+ // (tab hidden / pagehide) also trigger a save from the page.
69
+ const AUTOSAVE_MS = Math.max(0, parseInt(Q.get("autosave") || "30000", 10) || 0);
70
+ let KERNEL_KAPPA = null; // pins.kernel.sha256, captured at boot β€” stale-snapshot guard
71
+ let persisting = false; // single-flight guard around suspend()
72
+
73
+ // Adaptive tick: aim each run() chunk at ~TARGET_MS so the worker yields ~once per
74
+ // frame to deliver stdin β€” native-class input latency β€” while per-chunk overhead
75
+ // stays negligible.
76
+ const TARGET_MS = 8;
77
+ let budget = 2_000_000;
78
+
79
+ const status = (text) => postMessage({ type: "status", text });
80
+ const fail = (text, hint) => postMessage({ type: "error", text, hint });
81
+
82
+ // Boot artifacts are content-addressed + immutable and the ΞΊ-gate re-derives their SHA-256 on
83
+ // EVERY boot, so the HTTP cache is safe to lean on: a stale or corrupt cached byte simply fails
84
+ // the gate (Law L5) rather than booting. "force-cache" lets repeat boots skip the network entirely
85
+ // (cold boot still downloads once). The pins themselves are fetched no-store so they stay fresh.
86
+ const fetchBytes = async (url, cache = "force-cache") => {
87
+ const resp = await fetch(url, { cache });
88
+ if (!resp.ok) throw Object.assign(new Error(`HTTP ${resp.status}`), { status: resp.status });
89
+ // A static host (or a SW fallback) can answer a MISSING artifact with an HTML page (its 404/index)
90
+ // at HTTP 200 β€” which would then explode in JSON.parse as "Unexpected token '<'". Treat any HTML
91
+ // body where we expect a binary/JSON artifact as a clean "missing artifact" (404), so the boot fails
92
+ // gracefully ("Boot artifacts missing.") instead of leaking a raw parse error.
93
+ if (/^text\/html\b/i.test(resp.headers.get("content-type") || "")) {
94
+ throw Object.assign(new Error(`expected the artifact at ${url}, got an HTML page β€” it is not present on this deploy`), { status: 404 });
95
+ }
96
+ return new Uint8Array(await resp.arrayBuffer());
97
+ };
98
+ // Public IPFS gateways β€” content-addressed, NEVER trusted: gate() re-derives the whole-file sha256 below
99
+ // and refuses a mismatch (Law L5), so a wrong byte from any gateway is rejected, not booted.
100
+ const IPFS_GATEWAYS = ["https://ipfs.io", "https://dweb.link", "https://cloudflare-ipfs.com"];
101
+ // Fetch a boot artifact location-agnostically: prefer the same-origin path (dev / vendored image), else
102
+ // stream it from IPFS by its content (the ΞΊ-DAG CID pinned in kappa.json). The OS is agnostic to WHERE the
103
+ // bytes live β€” the ΞΊ is the only link β€” and the ΞΊ-gate verifies whatever source served them.
104
+ async function fetchArtifact(localUrl, ipfsCid) {
105
+ try { return await fetchBytes(localUrl); }
106
+ catch (e) {
107
+ if (!ipfsCid) throw e;
108
+ let last = e;
109
+ for (const gw of IPFS_GATEWAYS) {
110
+ try { return await fetchBytes(`${gw}/ipfs/${ipfsCid}`); } catch (g) { last = g; }
111
+ }
112
+ throw last;
113
+ }
114
+ }
115
+ const gunzip = async (bytes) =>
116
+ new Uint8Array(await new Response(new Response(bytes).body.pipeThrough(new DecompressionStream("gzip"))).arrayBuffer());
117
+ const gzip = async (bytes) =>
118
+ new Uint8Array(await new Response(new Response(bytes).body.pipeThrough(new CompressionStream("gzip"))).arrayBuffer());
119
+
120
+ // ── ΞΊ re-derivation (Law L5) ──────────────────────────────────────────────────
121
+ // The browser's own SubtleCrypto computes the content address β€” no engine, no
122
+ // trust. `did:holo:sha256:<hex>` is the ΞΊ; we compare it to the pin and to the
123
+ // canonical /boot pin. This is the whole point: bytes are admitted by identity.
124
+ const toHex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
125
+ async function sha256hex(bytes) {
126
+ return toHex(await crypto.subtle.digest("SHA-256", bytes));
127
+ }
128
+ // Verify one artifact against its ΞΊ-pin. Posts the result to the UI and THROWS on a
129
+ // mismatch β€” a failed gate must stop the boot, never warn-and-continue.
130
+ async function gate(which, pin, bytes) {
131
+ status(`re-deriving ΞΊ of the ${which}…`);
132
+ const actual = await sha256hex(bytes);
133
+ const ok = actual === pin.sha256;
134
+ postMessage({
135
+ type: "kappa", which, name: pin.name,
136
+ expected: "did:holo:sha256:" + pin.sha256,
137
+ actual: "did:holo:sha256:" + actual,
138
+ multibase: pin.digestMultibase, ok,
139
+ canonical: pin.canonicalPin || null,
140
+ });
141
+ if (!ok) {
142
+ throw Object.assign(
143
+ new Error(`ΞΊ mismatch on the ${which}: the fetched bytes do not match their content address.`),
144
+ { gate: true, which, expected: pin.sha256, actual },
145
+ );
146
+ }
147
+ }
148
+
149
+ // ── OPFS ΞΊ-disk helpers (the streamed, off-heap writable disk) ─────────────────
150
+ async function opfsSyncHandle(name, truncate) {
151
+ const root = await navigator.storage.getDirectory();
152
+ const fh = await root.getFileHandle(name, { create: true });
153
+ let lastErr;
154
+ for (let i = 0; i < 40; i++) { // ~8s of retries β€” a terminated worker frees its handle within ~1s
155
+ try { const h = await fh.createSyncAccessHandle(); if (truncate) h.truncate(0); return h; }
156
+ catch (e) { lastErr = e; await new Promise((r) => setTimeout(r, 200)); }
157
+ }
158
+ throw Object.assign(new Error(`the OPFS disk file "${name}" is locked by another session`), { opfsLocked: true, cause: lastErr });
159
+ }
160
+ async function opfsRemove(name) {
161
+ try { const root = await navigator.storage.getDirectory(); await root.removeEntry(name); } catch (_) {}
162
+ }
163
+ // GC dead tabs' ΞΊ-disks so multi-tab use doesn't leak OPFS. A live tab holds its
164
+ // pack handle β†’ removeEntry throws β†’ we leave that session alone. Never touches THIS session.
165
+ async function sweepDeadSessions() {
166
+ try {
167
+ const root = await navigator.storage.getDirectory();
168
+ const others = [];
169
+ for await (const name of root.keys()) { const m = /^hl-disk-(.+)\.kpack$/.exec(name); if (m && m[1] !== SID) others.push(m[1]); }
170
+ for (const sid of others) {
171
+ try { await root.removeEntry(`hl-disk-${sid}.kpack`); await opfsRemove(`hl-rootfs-${sid}.ext4`); } catch (_) {}
172
+ }
173
+ } catch (_) {}
174
+ }
175
+
176
+ // ── snapshot persistence helpers (CC-30 local persistence) ─────────────────────
177
+ // OPFS files are written via an exclusive SyncAccessHandle (synchronous, durable even
178
+ // under tab-close pressure). The snapshot files are SEPARATE from the running ΞΊ-disk
179
+ // pack, so saving never contends with the live machine's disk handle.
180
+ async function writeOpfs(name, bytes) {
181
+ const h = await opfsSyncHandle(name, true);
182
+ try { h.write(bytes, { at: 0 }); h.flush(); } finally { try { h.close(); } catch (_) {} }
183
+ }
184
+ async function readOpfsIfExists(name) {
185
+ const root = await navigator.storage.getDirectory();
186
+ try { await root.getFileHandle(name, { create: false }); } catch (_) { return null; } // absent β†’ no snapshot
187
+ const h = await opfsSyncHandle(name, false);
188
+ try { const n = h.getSize(); if (!n) return null; const b = new Uint8Array(n); h.read(b, { at: 0 }); return b; }
189
+ finally { try { h.close(); } catch (_) {} }
190
+ }
191
+ async function clearSnapshot() {
192
+ await opfsRemove(OPFS_STATE); await opfsRemove(OPFS_STATE_META); // legacy plaintext
193
+ await opfsRemove(OPFS_SEALED); await opfsRemove(OPFS_SEALED_META); // sealed
194
+ }
195
+
196
+ // ── device-bound machine keys (IndexedDB, NON-EXTRACTABLE) ──────────────────────
197
+ // SOFT tier of the sovereign machine: an AES-GCM seal key + an ECDSA P-256 owner key, both
198
+ // non-extractable and device-local. The raw key bytes never reach JS and never touch OPFS, so a
199
+ // snapshot copied to another device can't be decrypted (no key there) or forged (can't sign).
200
+ // On this device the machine resumes transparently. TEE upgrade (enclave-gated key + biometric
201
+ // step-up whose challenge is the snapshot ΞΊ) is the next milestone β€” attestRoot will become "tee".
202
+ const b64 = (u8) => btoa(String.fromCharCode(...new Uint8Array(u8)));
203
+ const b64d = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
204
+ const KEYDB = "holo-linux-keys", KEYSTORE = "keys";
205
+ function idbOpen() { return new Promise((res, rej) => { const r = indexedDB.open(KEYDB, 1); r.onupgradeneeded = () => r.result.createObjectStore(KEYSTORE); r.onsuccess = () => res(r.result); r.onerror = () => rej(r.error); }); }
206
+ async function idbGet(k) { const db = await idbOpen(); return new Promise((res, rej) => { const t = db.transaction(KEYSTORE, "readonly").objectStore(KEYSTORE).get(k); t.onsuccess = () => res(t.result || null); t.onerror = () => rej(t.error); }); }
207
+ async function idbPut(k, v) { const db = await idbOpen(); return new Promise((res, rej) => { const t = db.transaction(KEYSTORE, "readwrite").objectStore(KEYSTORE).put(v, k); t.onsuccess = () => res(); t.onerror = () => rej(t.error); }); }
208
+ let machineKeys = null; // {aes, sign(priv), verify(pub), pubRaw, attestRoot}
209
+ // TEE unlock: the page performs the device biometric (WebAuthn PRF / native holo:hello) and posts the PRF
210
+ // secret here BEFORE keys are used (boot awaits it when ?tee=1). The secret is held in memory only, never
211
+ // persisted β€” the seal key is re-derived from it per session, so the machine is bound to your biometric.
212
+ let unlockSecret = null, _unlockResolve; const unlockReady = new Promise((r) => (_unlockResolve = r));
213
+ // HKDF the biometric PRF secret β†’ the AES-GCM seal key. Deterministic (same secret β‡’ same key β‡’ opens);
214
+ // a different biometric/identity derives a different key and AEAD refuses. Verified in Node (tee-crypto-check).
215
+ async function aesFromSecret(secret) {
216
+ const ikm = await crypto.subtle.importKey("raw", secret, "HKDF", false, ["deriveKey"]);
217
+ return crypto.subtle.deriveKey(
218
+ { name: "HKDF", hash: "SHA-256", salt: new TextEncoder().encode(SID + "|holo-linux/seal/v1"), info: new TextEncoder().encode("aes-gcm") },
219
+ ikm, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
220
+ }
221
+ async function ensureMachineKeys() {
222
+ if (machineKeys) return machineKeys;
223
+ // The owner SIGNING key (ownership) + the SOFT seal key are device-local + non-extractable (IndexedDB).
224
+ let rec = await idbGet("machine-" + SID);
225
+ const soft = rec && (rec.softAes || rec.aes);
226
+ if (!rec || !rec.priv || !rec.pub || !rec.pubRaw || !soft) {
227
+ const softAes = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
228
+ const kp = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, ["sign", "verify"]); // pubKey stays extractable per spec
229
+ const pubRaw = new Uint8Array(await crypto.subtle.exportKey("raw", kp.publicKey));
230
+ rec = { softAes, priv: kp.privateKey, pub: kp.publicKey, pubRaw };
231
+ await idbPut("machine-" + SID, rec);
232
+ }
233
+ // Seal key source: TEE (HKDF from the biometric PRF secret) when unlocked, else the device-local soft key.
234
+ let aes, attestRoot;
235
+ if (unlockSecret) { aes = await aesFromSecret(unlockSecret); attestRoot = "tee"; }
236
+ else { aes = rec.softAes || rec.aes; attestRoot = "soft"; }
237
+ machineKeys = { aes, sign: rec.priv, verify: rec.pub, pubRaw: new Uint8Array(rec.pubRaw), attestRoot };
238
+ return machineKeys;
239
+ }
240
+ // Encrypt + owner-sign the gzipped snapshot. Signature binds BOTH the ciphertext ΞΊ and the plaintext ΞΊ.
241
+ async function sealBytes(gz, kappaHex) {
242
+ const k = await ensureMachineKeys();
243
+ const iv = crypto.getRandomValues(new Uint8Array(12));
244
+ const ct = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, k.aes, gz));
245
+ const sealedKappa = await sha256hex(ct);
246
+ const msg = new TextEncoder().encode(sealedKappa + ":" + kappaHex);
247
+ const sig = new Uint8Array(await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, k.sign, msg));
248
+ return { iv, ct, sealedKappa, sig: b64(sig), pub: b64(k.pubRaw), attestRoot: k.attestRoot };
249
+ }
250
+ // Verify ownership + integrity, then decrypt. Throws {sovereign:true, reason} on any failure (foreign
251
+ // device, wrong identity, tamper) so the caller refuses + recovers (Law L5 / SEC-1 / SEC-4).
252
+ async function openSealed(meta, iv, ct) {
253
+ if (meta.attestRoot === "tee" && !unlockSecret) throw { sovereign: true, reason: "this machine is sealed to your biometric β€” unlock required" };
254
+ const k = await ensureMachineKeys();
255
+ if (!meta.pub || meta.pub !== b64(k.pubRaw)) throw { sovereign: true, reason: "this saved machine is bound to a different device or identity" };
256
+ const sealedKappa = await sha256hex(ct);
257
+ if (sealedKappa !== meta.sealedKappa) throw { sovereign: true, reason: "sealed bytes failed their ΞΊ (tampered)" };
258
+ const msg = new TextEncoder().encode(sealedKappa + ":" + meta.kappa);
259
+ const ok = await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, k.verify, b64d(meta.sig), msg);
260
+ if (!ok) throw { sovereign: true, reason: "owner signature invalid" };
261
+ const gz = new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, k.aes, ct));
262
+ const snap = await gunzip(gz);
263
+ if ((await sha256hex(snap)) !== meta.kappa) throw { sovereign: true, reason: "plaintext ΞΊ mismatch" };
264
+ return snap;
265
+ }
266
+
267
+ // Capture the running machine, SEAL it (encrypt + owner-sign), and persist. Content-addressed (Law L5);
268
+ // opaque at rest (SEC-5). Single-flight; safe between run() ticks (the worker is single-threaded).
269
+ async function persistSnapshot(reason) {
270
+ if (!ws || EPHEMERAL || persisting) return;
271
+ persisting = true;
272
+ try {
273
+ const snap = ws.suspend(); // CPU + RAM + disk + 9p workspace files
274
+ const kappa = await sha256hex(snap);
275
+ const gz = await gzip(snap);
276
+ const sealed = await sealBytes(gz, kappa);
277
+ const blob = new Uint8Array(12 + sealed.ct.length); blob.set(sealed.iv, 0); blob.set(sealed.ct, 12);
278
+ await writeOpfs(OPFS_SEALED, blob);
279
+ const meta = { kappa, sealedKappa: sealed.sealedKappa, sig: sealed.sig, pub: sealed.pub,
280
+ alg: "AES-256-GCM+ECDSA-P256", attestRoot: sealed.attestRoot, kernelKappa: KERNEL_KAPPA,
281
+ bytes: snap.length, sealedBytes: blob.length, createdAt: Date.now(), reason: reason || "manual" };
282
+ await writeOpfs(OPFS_SEALED_META, new TextEncoder().encode(JSON.stringify(meta)));
283
+ await opfsRemove(OPFS_STATE); await opfsRemove(OPFS_STATE_META); // drop any legacy plaintext
284
+ postMessage({ type: "suspended", kappa: "did:holo:sha256:" + kappa, bytes: snap.length, gzBytes: blob.length, attestRoot: sealed.attestRoot, reason: reason || "manual" });
285
+ } catch (e) {
286
+ postMessage({ type: "persist-error", text: String((e && e.message) || e) });
287
+ } finally { persisting = false; }
288
+ }
289
+
290
+ // Smallest ΞΊ-disk we'll fall back to when storage is tight β€” still comfortably fits the
291
+ // Debian rootfs (~70 MiB used) plus working room. Below this we surface a clear error.
292
+ const DISK_FLOOR = 384 * 1024 * 1024;
293
+ const isSpaceError = (e) => {
294
+ const m = String((e && (e.message || e.name)) || e);
295
+ return (e && e.name === "QuotaExceededError") || /no space|quota|insufficient|allocat/i.test(m);
296
+ };
297
+ // Best-effort: persistent storage isn't evicted under pressure and tends to grant a larger,
298
+ // stabler quota. Not available in every worker context β€” ignore if absent.
299
+ async function requestPersistence() {
300
+ try { if (navigator.storage && navigator.storage.persist) await navigator.storage.persist(); } catch (_) {}
301
+ }
302
+ // Size the ΞΊ-disk to the space actually free RIGHT NOW. OPFS shares ONE origin quota with the
303
+ // service-worker caches and every other holospace, so a fixed 1 GiB truncate fails with "No
304
+ // space available" when the origin is near quota. Fit to free space (less a safety margin),
305
+ // never below the floor; the caller still retries smaller if even this overshoots.
306
+ async function fitDiskBytes(requested) {
307
+ try {
308
+ const est = (await navigator.storage.estimate()) || {};
309
+ const quota = est.quota || 0, usage = est.usage || 0;
310
+ if (quota > 0) {
311
+ const usable = (quota - usage) - 96 * 1024 * 1024; // keep 96 MiB headroom for fs metadata + caches
312
+ if (usable < requested) return Math.max(DISK_FLOOR, Math.min(requested, usable));
313
+ }
314
+ } catch (_) {}
315
+ return requested;
316
+ }
317
+ // Provision a fresh rootfs handle + assemble the bootable image into it. Fresh OPFS files each
318
+ // attempt, so a retry never inherits a half-written disk. Closes the handle and rethrows on
319
+ // failure so the caller can reclaim space and retry at a smaller size.
320
+ async function provisionRootfs(image, diskBytes) {
321
+ await opfsRemove(OPFS_ROOTFS);
322
+ await opfsRemove(OPFS_DISK_PACK);
323
+ const h = await opfsSyncHandle(OPFS_ROOTFS, true);
324
+ try { image.assemble_bootable_into_opfs(h, diskBytes, true); return h; } // REAL_IMG=true β†’ execs /bin/bash -l
325
+ catch (e) { try { h.close(); } catch (_) {} throw e; }
326
+ }
327
+
328
+ let ws = null; // the running Workspace
329
+ const pendingInput = []; // keystrokes that arrived before boot finished
330
+
331
+ self.onmessage = (e) => {
332
+ const msg = e.data;
333
+ if (!msg) return;
334
+ if (msg.type === "stdin") { if (ws) ws.feed_input(msg.data); else pendingInput.push(msg.data); }
335
+ else if (msg.type === "unlock") { unlockSecret = msg.secret ? new Uint8Array(msg.secret) : null; machineKeys = null; _unlockResolve(); } // biometric PRF secret from the page; drop cached keys so the next seal re-derives (soft→tee "make sovereign" re-seals live state under the TEE key)
336
+ else if (msg.type === "suspend") { persistSnapshot(msg.reason || "request"); } // page asks us to save (button / tab hidden / pagehide)
337
+ else if (msg.type === "reset") { clearSnapshot().then(() => postMessage({ type: "reset-done" })); }
338
+ };
339
+
340
+ (async () => {
341
+ try {
342
+ status("loading the machine…");
343
+ await init(new URL("/pkg/holospaces_web_bg.wasm?v=hl1", location.origin));
344
+ await requestPersistence();
345
+ await sweepDeadSessions();
346
+ if (RESET) { await clearSnapshot(); status("starting a fresh machine…"); }
347
+
348
+ // ── ΞΊ-pins first β€” needed BOTH to stale-check a saved machine (kernel ΞΊ) and to
349
+ // gate a cold boot. Small + no-store so they stay fresh. ────────────────────
350
+ status("fetching the ΞΊ-pins…");
351
+ let pins;
352
+ try { pins = JSON.parse(new TextDecoder().decode(await fetchBytes(PINS_URL, "no-store"))); }
353
+ catch (err) {
354
+ if (err.status === 404) return fail("Boot artifacts missing.", "kappa.json must sit beside this worker.");
355
+ throw err;
356
+ }
357
+ KERNEL_KAPPA = (pins.kernel && pins.kernel.sha256) || null;
358
+
359
+ // TEE unlock handshake: when the page signalled it has a biometric (?tee=1), wait briefly for the PRF
360
+ // secret before any key use, so the seal key is enclave-derived. Times out β†’ soft fallback (still sealed).
361
+ if (Q.get("tee")) { await Promise.race([unlockReady, new Promise((r) => setTimeout(r, 8000))]); }
362
+
363
+ // ── RESUME PATH (sovereign machine): if a SEALED saved machine exists, verify ownership +
364
+ // integrity (owner signature + L5 ΞΊ, bound to this device's non-extractable key), DECRYPT,
365
+ // and resume it exactly β€” skipping the whole cold boot. A foreign/tampered/wrong-kernel
366
+ // machine is refused and we recover by cold-booting. ?tamper forces a cold boot (rootfs
367
+ // tamper test); ?tampersnap flips a ciphertext byte to exercise THIS gate. ───────────────
368
+ if (!EPHEMERAL && !RESET && !Q.get("tamper")) {
369
+ try {
370
+ const metaBytes = await readOpfsIfExists(OPFS_SEALED_META);
371
+ const sealed = await readOpfsIfExists(OPFS_SEALED);
372
+ if (metaBytes && sealed) {
373
+ const meta = JSON.parse(new TextDecoder().decode(metaBytes));
374
+ status("found your sealed machine β€” verifying owner + κ…");
375
+ const iv = sealed.slice(0, 12);
376
+ const ct = sealed.slice(12);
377
+ if (Q.get("tampersnap")) { ct[0] ^= 0xff; status("⚠ tamper test β€” flipped 1 byte of the sealed machine; the gate must refuse it"); }
378
+ const kernelOk = !meta.kernelKappa || meta.kernelKappa === KERNEL_KAPPA;
379
+ if (!kernelOk) {
380
+ status("kernel changed since the saved machine β€” booting fresh");
381
+ await clearSnapshot();
382
+ } else {
383
+ const snap = await openSealed(meta, iv, ct); // throws {sovereign} on foreign/tamper/forged
384
+ postMessage({ type: "kappa", which: "snapshot", name: "sealed machine",
385
+ expected: "did:holo:sha256:" + meta.kappa, actual: "did:holo:sha256:" + meta.kappa,
386
+ ok: true, canonical: meta.attestRoot === "tee" ? "device TEE" : "device key" });
387
+ status("resuming your sealed machine…");
388
+ ws = Workspace.resume_devcontainer(snap);
389
+ postMessage({ type: "resumed", kappa: "did:holo:sha256:" + meta.kappa, bytes: snap.length, gzBytes: sealed.length, attestRoot: meta.attestRoot });
390
+ }
391
+ }
392
+ } catch (e) {
393
+ const sov = e && e.sovereign;
394
+ if (sov) postMessage({ type: "kappa", which: "snapshot", name: "sealed machine", expected: "β€”", actual: "β€”", ok: false, canonical: null });
395
+ postMessage({ type: "persist-error", text: (sov ? e.reason : "resume failed (" + String((e && e.message) || e) + ")") + " β€” booting fresh" });
396
+ await clearSnapshot(); // can't open it (foreign/tampered/lost key) β†’ recover with a fresh machine
397
+ ws = null;
398
+ }
399
+ }
400
+
401
+ // ── COLD BOOT: no saved machine (or reset / tamper) β€” fetch, gate, assemble, boot ──
402
+ if (!ws) {
403
+ status("fetching the ΞΊ-pinned kernel + Debian rootfs…");
404
+ let kernelGz, rootfsLayer;
405
+ try {
406
+ [kernelGz, rootfsLayer] = await Promise.all([fetchArtifact(KERNEL_URL, pins.kernel && pins.kernel.ipfs), fetchArtifact(ROOTFS_URL, pins.rootfs && pins.rootfs.ipfs)]);
407
+ } catch (err) {
408
+ if (err.status === 404) return fail("Boot artifacts missing.", "os-kernel.gz / os-rootfs.tar.gz / kappa.json must sit beside this worker.");
409
+ throw err;
410
+ }
411
+
412
+ // Tamper test (?tamper=1): flip ONE byte of the rootfs before the gate, to prove
413
+ // the ΞΊ-gate is real β€” the very next step re-derives the address and must refuse.
414
+ if (Q.get("tamper")) {
415
+ rootfsLayer[0] ^= 0xff;
416
+ status("⚠ tamper test β€” flipped 1 byte of the rootfs; the ΞΊ-gate must now refuse it");
417
+ }
418
+
419
+ // THE ΞΊ-GATE: re-derive content addresses, refuse to boot on mismatch.
420
+ await gate("kernel", pins.kernel, kernelGz);
421
+ await gate("rootfs", pins.rootfs, rootfsLayer);
422
+ status("ΞΊ verified β€” both artifacts match their content address.");
423
+
424
+ // assemble the bootable ext4 from the verified rootfs layer
425
+ const kernel = await gunzip(kernelGz);
426
+ status("assembling the root filesystem…");
427
+ const image = new DevcontainerImage();
428
+ image.add_layer(LAYER_MEDIA, rootfsLayer); // the in-engine assembler gunzips + untars it
429
+
430
+ // provision the ΞΊ-disk into OPFS (sparse, off-heap) and boot. Robust against a near-full
431
+ // origin quota: size to free space, and on "No space" reclaim dead sessions + retry smaller.
432
+ status("provisioning the ΞΊ-disk (sparse, OPFS-backed)…");
433
+ let diskBytes = await fitDiskBytes(DISK_BYTES);
434
+ let rootfsH;
435
+ for (let attempt = 0; ; attempt++) {
436
+ try { rootfsH = await provisionRootfs(image, diskBytes); break; }
437
+ catch (err) {
438
+ if (isSpaceError(err) && attempt < 3 && diskBytes > DISK_FLOOR) {
439
+ status("storage is tight β€” reclaiming space, retrying with a smaller ΞΊ-disk…");
440
+ await sweepDeadSessions();
441
+ diskBytes = Math.max(DISK_FLOOR, Math.floor(diskBytes / 2));
442
+ continue;
443
+ }
444
+ throw err;
445
+ }
446
+ }
447
+ const diskH = await opfsSyncHandle(OPFS_DISK_PACK, true);
448
+ status("powering on (riscv64, streamed ΞΊ-disk)…");
449
+ ws = Workspace.boot_devcontainer_opfs_streamed(kernel, rootfsH, diskH);
450
+ }
451
+
452
+ for (const buf of pendingInput) ws.feed_input(buf);
453
+ pendingInput.length = 0;
454
+
455
+ const bootStart = performance.now();
456
+ postMessage({ type: "booted" });
457
+ // Crash-resilient autosave: snapshot the running machine to OPFS every AUTOSAVE_MS so a
458
+ // crash/forced-close still leaves a recent machine to resume. Lifecycle saves (tab hidden /
459
+ // pagehide) come from the page via {type:'suspend'}.
460
+ if (AUTOSAVE_MS && !EPHEMERAL) setInterval(() => persistSnapshot("autosave"), AUTOSAVE_MS);
461
+
462
+ // A zero-delay yield that isn't throttled to 4ms (unlike setTimeout(0)) β€” the
463
+ // run loop stays continuous in ~TARGET_MS chunks, the event loop runs between
464
+ // chunks to deliver stdin.
465
+ const yieldChan = new MessageChannel();
466
+ yieldChan.port1.onmessage = () => tick();
467
+ const scheduleTick = () => yieldChan.port2.postMessage(0);
468
+
469
+ let lastReport = bootStart, instret = 0;
470
+ const tick = () => {
471
+ const t0 = performance.now();
472
+ let halted = false;
473
+ try { halted = ws.run(budget); } catch (err) { return fail(`run: ${err && err.message ? err.message : err}`); }
474
+ instret += budget;
475
+ const dt = performance.now() - t0;
476
+ if (dt > 0.1) budget = Math.max(100_000, Math.min(80_000_000, Math.round((budget * TARGET_MS) / dt)));
477
+ const out = ws.terminal_delta();
478
+ if (out.length) postMessage({ type: "stdout", data: out }, [out.buffer]);
479
+ const now = performance.now();
480
+ if (now - lastReport > 400) { postMessage({ type: "mips", mips: instret / ((now - bootStart) / 1000) / 1e6 }); lastReport = now; }
481
+ if (halted) postMessage({ type: "halt", reason: "the guest powered off" });
482
+ else scheduleTick();
483
+ };
484
+ tick();
485
+ } catch (err) {
486
+ const msg = String(err && err.message ? err.message : err);
487
+ if (err && err.gate) {
488
+ return fail(msg, `Holo Linux refuses to execute bytes that do not match their ΞΊ.\nexpected did:holo:sha256:${err.expected}\nactual did:holo:sha256:${err.actual}`);
489
+ }
490
+ if (isSpaceError(err)) {
491
+ return fail("Browser storage is full β€” couldn't allocate the ΞΊ-disk.",
492
+ "OPFS shares one quota across every Holo app. Close other Holo Linux / Holo tabs (each holds its own disk) or clear this site's data, then press ⟳ reboot. You can also boot a smaller disk with ?disk=256.");
493
+ }
494
+ const locked = (err && err.opfsLocked) || /createSyncAccessHandle|Access Handle/i.test(msg);
495
+ fail(locked ? "The disk is open in another tab." : msg,
496
+ locked ? "OPFS gives the κ-disk to one tab at a time. Close other Holo Linux tabs, then press ⟳ reboot." : undefined);
497
+ }
498
+ })();
b/3cd10cb95409eb044bdc15bbfeae1d380d98e4cb6b5635e3d7e5979d30eba593 ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # fingerprint β€” every byte is admitted by its content address (ΞΊ), not by trust (Law L5).
3
+ # The host re-derives the SHA-256 of the kernel + rootfs and refuses to boot on any mismatch;
4
+ # here we compute the same kind of content addresses LIVE, from inside the ΞΊ-verified guest.
5
+
6
+ A=$'\033[1;38;5;51m'; D=$'\033[38;5;244m'; B=$'\033[38;5;39m'; R=$'\033[0m'
7
+
8
+ printf '\n%sHolo Linux is ΞΊ-addressable%s\n\n' "$A" "$R"
9
+
10
+ printf '%sThe kernel and rootfs booted only because their SHA-256 matched the content\n' "$D"
11
+ printf 'address pinned in kappa.json β€” re-derived before the first instruction ran.%s\n\n' "$R"
12
+
13
+ printf '%scontent address (sha256) of files in this ΞΊ-verified rootfs:%s\n' "$B" "$R"
14
+ sha256sum /etc/os-release /etc/profile.d/00-holo-linux.sh /usr/local/bin/verify 2>/dev/null | sed 's/^/ /'
15
+
16
+ printf '\n%sIdentity by content, not by hostname, server, or trust. See the βœ“ ΞΊ-anchored\n' "$D"
17
+ printf 'rail above the terminal for the kernel + rootfs ΞΊ, re-derived on THIS boot.%s\n\n' "$R"
b/456ac33710c2dee959da5703727d01fbf5350a51ac24077488ca1a9a0332e08f ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SearchAddon=t():e.SearchAddon=t()}(self,(()=>(()=>{"use strict";var e={345:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.runAndSubscribe=t.forwardEvent=t.EventEmitter=void 0,t.EventEmitter=class{constructor(){this._listeners=[],this._disposed=!1}get event(){return this._event||(this._event=e=>(this._listeners.push(e),{dispose:()=>{if(!this._disposed)for(let t=0;t<this._listeners.length;t++)if(this._listeners[t]===e)return void this._listeners.splice(t,1)}})),this._event}fire(e,t){const i=[];for(let e=0;e<this._listeners.length;e++)i.push(this._listeners[e]);for(let s=0;s<i.length;s++)i[s].call(void 0,e,t)}dispose(){this.clearListeners(),this._disposed=!0}clearListeners(){this._listeners&&(this._listeners.length=0)}},t.forwardEvent=function(e,t){return e((e=>t.fire(e)))},t.runAndSubscribe=function(e,t){return t(void 0),e((e=>t(e)))}},859:(e,t)=>{function i(e){for(const t of e)t.dispose();e.length=0}Object.defineProperty(t,"__esModule",{value:!0}),t.getDisposeArrayDisposable=t.disposeArray=t.toDisposable=t.MutableDisposable=t.Disposable=void 0,t.Disposable=class{constructor(){this._disposables=[],this._isDisposed=!1}dispose(){this._isDisposed=!0;for(const e of this._disposables)e.dispose();this._disposables.length=0}register(e){return this._disposables.push(e),e}unregister(e){const t=this._disposables.indexOf(e);-1!==t&&this._disposables.splice(t,1)}},t.MutableDisposable=class{constructor(){this._isDisposed=!1}get value(){return this._isDisposed?void 0:this._value}set value(e){this._isDisposed||e===this._value||(this._value?.dispose(),this._value=e)}clear(){this.value=void 0}dispose(){this._isDisposed=!0,this._value?.dispose(),this._value=void 0}},t.toDisposable=function(e){return{dispose:e}},t.disposeArray=i,t.getDisposeArrayDisposable=function(e){return{dispose:()=>i(e)}}}},t={};function i(s){var r=t[s];if(void 0!==r)return r.exports;var o=t[s]={exports:{}};return e[s](o,o.exports,i),o.exports}var s={};return(()=>{var e=s;Object.defineProperty(e,"__esModule",{value:!0}),e.SearchAddon=void 0;const t=i(345),r=i(859),o=" ~!@#$%^&*()+`-=[]{}|\\;:\"',./<>?";class n extends r.Disposable{constructor(e){super(),this._highlightedLines=new Set,this._highlightDecorations=[],this._selectedDecoration=this.register(new r.MutableDisposable),this._linesCacheTimeoutId=0,this._linesCacheDisposables=new r.MutableDisposable,this._onDidChangeResults=this.register(new t.EventEmitter),this.onDidChangeResults=this._onDidChangeResults.event,this._highlightLimit=e?.highlightLimit??1e3}activate(e){this._terminal=e,this.register(this._terminal.onWriteParsed((()=>this._updateMatches()))),this.register(this._terminal.onResize((()=>this._updateMatches()))),this.register((0,r.toDisposable)((()=>this.clearDecorations())))}_updateMatches(){this._highlightTimeout&&window.clearTimeout(this._highlightTimeout),this._cachedSearchTerm&&this._lastSearchOptions?.decorations&&(this._highlightTimeout=setTimeout((()=>{const e=this._cachedSearchTerm;this._cachedSearchTerm=void 0,this.findPrevious(e,{...this._lastSearchOptions,incremental:!0,noScroll:!0})}),200))}clearDecorations(e){this._selectedDecoration.clear(),(0,r.disposeArray)(this._highlightDecorations),this._highlightDecorations=[],this._highlightedLines.clear(),e||(this._cachedSearchTerm=void 0)}clearActiveDecoration(){this._selectedDecoration.clear()}findNext(e,t){if(!this._terminal)throw new Error("Cannot use addon until it has been loaded");const i=!this._lastSearchOptions||this._didOptionsChange(this._lastSearchOptions,t);this._lastSearchOptions=t,t?.decorations&&(void 0===this._cachedSearchTerm||e!==this._cachedSearchTerm||i)&&this._highlightAllMatches(e,t);const s=this._findNextAndSelect(e,t);return this._fireResults(t),this._cachedSearchTerm=e,s}_highlightAllMatches(e,t){if(!this._terminal)throw new Error("Cannot use addon until it has been loaded");if(!e||0===e.length)return void this.clearDecorations();t=t||{},this.clearDecorations(!0);const i=[];let s,r=this._find(e,0,0,t);for(;r&&(s?.row!==r.row||s?.col!==r.col)&&!(i.length>=this._highlightLimit);)s=r,i.push(s),r=this._find(e,s.col+s.term.length>=this._terminal.cols?s.row+1:s.row,s.col+s.term.length>=this._terminal.cols?0:s.col+1,t);for(const e of i){const i=this._createResultDecoration(e,t.decorations);i&&(this._highlightedLines.add(i.marker.line),this._highlightDecorations.push({decoration:i,match:e,dispose(){i.dispose()}}))}}_find(e,t,i,s){if(!this._terminal||!e||0===e.length)return this._terminal?.clearSelection(),void this.clearDecorations();if(i>this._terminal.cols)throw new Error(`Invalid col: ${i} to search in terminal of ${this._terminal.cols} cols`);let r;this._initLinesCache();const o={startRow:t,startCol:i};if(r=this._findInLine(e,o,s),!r)for(let i=t+1;i<this._terminal.buffer.active.baseY+this._terminal.rows&&(o.startRow=i,o.startCol=0,r=this._findInLine(e,o,s),!r);i++);return r}_findNextAndSelect(e,t){if(!this._terminal||!e||0===e.length)return this._terminal?.clearSelection(),this.clearDecorations(),!1;const i=this._terminal.getSelectionPosition();this._terminal.clearSelection();let s=0,r=0;i&&(this._cachedSearchTerm===e?(s=i.end.x,r=i.end.y):(s=i.start.x,r=i.start.y)),this._initLinesCache();const o={startRow:r,startCol:s};let n=this._findInLine(e,o,t);if(!n)for(let i=r+1;i<this._terminal.buffer.active.baseY+this._terminal.rows&&(o.startRow=i,o.startCol=0,n=this._findInLine(e,o,t),!n);i++);if(!n&&0!==r)for(let i=0;i<r&&(o.startRow=i,o.startCol=0,n=this._findInLine(e,o,t),!n);i++);return!n&&i&&(o.startRow=i.start.y,o.startCol=0,n=this._findInLine(e,o,t)),this._selectResult(n,t?.decorations,t?.noScroll)}findPrevious(e,t){if(!this._terminal)throw new Error("Cannot use addon until it has been loaded");const i=!this._lastSearchOptions||this._didOptionsChange(this._lastSearchOptions,t);this._lastSearchOptions=t,t?.decorations&&(void 0===this._cachedSearchTerm||e!==this._cachedSearchTerm||i)&&this._highlightAllMatches(e,t);const s=this._findPreviousAndSelect(e,t);return this._fireResults(t),this._cachedSearchTerm=e,s}_didOptionsChange(e,t){return!!t&&(e.caseSensitive!==t.caseSensitive||e.regex!==t.regex||e.wholeWord!==t.wholeWord)}_fireResults(e){if(e?.decorations){let e=-1;if(this._selectedDecoration.value){const t=this._selectedDecoration.value.match;for(let i=0;i<this._highlightDecorations.length;i++){const s=this._highlightDecorations[i].match;if(s.row===t.row&&s.col===t.col&&s.size===t.size){e=i;break}}}this._onDidChangeResults.fire({resultIndex:e,resultCount:this._highlightDecorations.length})}}_findPreviousAndSelect(e,t){if(!this._terminal)throw new Error("Cannot use addon until it has been loaded");if(!this._terminal||!e||0===e.length)return this._terminal?.clearSelection(),this.clearDecorations(),!1;const i=this._terminal.getSelectionPosition();this._terminal.clearSelection();let s=this._terminal.buffer.active.baseY+this._terminal.rows-1,r=this._terminal.cols;const o=!0;this._initLinesCache();const n={startRow:s,startCol:r};let h;if(i&&(n.startRow=s=i.start.y,n.startCol=r=i.start.x,this._cachedSearchTerm!==e&&(h=this._findInLine(e,n,t,!1),h||(n.startRow=s=i.end.y,n.startCol=r=i.end.x))),h||(h=this._findInLine(e,n,t,o)),!h){n.startCol=Math.max(n.startCol,this._terminal.cols);for(let i=s-1;i>=0&&(n.startRow=i,h=this._findInLine(e,n,t,o),!h);i--);}if(!h&&s!==this._terminal.buffer.active.baseY+this._terminal.rows-1)for(let i=this._terminal.buffer.active.baseY+this._terminal.rows-1;i>=s&&(n.startRow=i,h=this._findInLine(e,n,t,o),!h);i--);return this._selectResult(h,t?.decorations,t?.noScroll)}_initLinesCache(){const e=this._terminal;this._linesCache||(this._linesCache=new Array(e.buffer.active.length),this._linesCacheDisposables.value=(0,r.getDisposeArrayDisposable)([e.onLineFeed((()=>this._destroyLinesCache())),e.onCursorMove((()=>this._destroyLinesCache())),e.onResize((()=>this._destroyLinesCache()))])),window.clearTimeout(this._linesCacheTimeoutId),this._linesCacheTimeoutId=window.setTimeout((()=>this._destroyLinesCache()),15e3)}_destroyLinesCache(){this._linesCache=void 0,this._linesCacheDisposables.clear(),this._linesCacheTimeoutId&&(window.clearTimeout(this._linesCacheTimeoutId),this._linesCacheTimeoutId=0)}_isWholeWord(e,t,i){return(0===e||o.includes(t[e-1]))&&(e+i.length===t.length||o.includes(t[e+i.length]))}_findInLine(e,t,i={},s=!1){const r=this._terminal,o=t.startRow,n=t.startCol,h=r.buffer.active.getLine(o);if(h?.isWrapped)return s?void(t.startCol+=r.cols):(t.startRow--,t.startCol+=r.cols,this._findInLine(e,t,i));let a=this._linesCache?.[o];a||(a=this._translateBufferLineToStringWithWrap(o,!0),this._linesCache&&(this._linesCache[o]=a));const[l,c]=a,d=this._bufferColsToStringOffset(o,n),_=i.caseSensitive?e:e.toLowerCase(),u=i.caseSensitive?l:l.toLowerCase();let f=-1;if(i.regex){const t=RegExp(_,"g");let i;if(s)for(;i=t.exec(u.slice(0,d));)f=t.lastIndex-i[0].length,e=i[0],t.lastIndex-=e.length-1;else i=t.exec(u.slice(d)),i&&i[0].length>0&&(f=d+(t.lastIndex-i[0].length),e=i[0])}else s?d-_.length>=0&&(f=u.lastIndexOf(_,d-_.length)):f=u.indexOf(_,d);if(f>=0){if(i.wholeWord&&!this._isWholeWord(f,u,e))return;let t=0;for(;t<c.length-1&&f>=c[t+1];)t++;let s=t;for(;s<c.length-1&&f+e.length>=c[s+1];)s++;const n=f-c[t],h=f+e.length-c[s],a=this._stringLengthToBufferSize(o+t,n);return{term:e,col:a,row:o+t,size:this._stringLengthToBufferSize(o+s,h)-a+r.cols*(s-t)}}}_stringLengthToBufferSize(e,t){const i=this._terminal.buffer.active.getLine(e);if(!i)return 0;for(let e=0;e<t;e++){const s=i.getCell(e);if(!s)break;const r=s.getChars();r.length>1&&(t-=r.length-1);const o=i.getCell(e+1);o&&0===o.getWidth()&&t++}return t}_bufferColsToStringOffset(e,t){const i=this._terminal;let s=e,r=0,o=i.buffer.active.getLine(s);for(;t>0&&o;){for(let e=0;e<t&&e<i.cols;e++){const t=o.getCell(e);if(!t)break;t.getWidth()&&(r+=0===t.getCode()?1:t.getChars().length)}if(s++,o=i.buffer.active.getLine(s),o&&!o.isWrapped)break;t-=i.cols}return r}_translateBufferLineToStringWithWrap(e,t){const i=this._terminal,s=[],r=[0];let o=i.buffer.active.getLine(e);for(;o;){const n=i.buffer.active.getLine(e+1),h=!!n&&n.isWrapped;let a=o.translateToString(!h&&t);if(h&&n){const e=o.getCell(o.length-1);e&&0===e.getCode()&&1===e.getWidth()&&2===n.getCell(0)?.getWidth()&&(a=a.slice(0,-1))}if(s.push(a),!h)break;r.push(r[r.length-1]+a.length),e++,o=n}return[s.join(""),r]}_selectResult(e,t,i){const s=this._terminal;if(this._selectedDecoration.clear(),!e)return s.clearSelection(),!1;if(s.select(e.col,e.row,e.size),t){const i=s.registerMarker(-s.buffer.active.baseY-s.buffer.active.cursorY+e.row);if(i){const o=s.registerDecoration({marker:i,x:e.col,width:e.size,backgroundColor:t.activeMatchBackground,layer:"top",overviewRulerOptions:{color:t.activeMatchColorOverviewRuler}});if(o){const s=[];s.push(i),s.push(o.onRender((e=>this._applyStyles(e,t.activeMatchBorder,!0)))),s.push(o.onDispose((()=>(0,r.disposeArray)(s)))),this._selectedDecoration.value={decoration:o,match:e,dispose(){o.dispose()}}}}}if(!i&&(e.row>=s.buffer.active.viewportY+s.rows||e.row<s.buffer.active.viewportY)){let t=e.row-s.buffer.active.viewportY;t-=Math.floor(s.rows/2),s.scrollLines(t)}return!0}_applyStyles(e,t,i){e.classList.contains("xterm-find-result-decoration")||(e.classList.add("xterm-find-result-decoration"),t&&(e.style.outline=`1px solid ${t}`)),i&&e.classList.add("xterm-find-active-result-decoration")}_createResultDecoration(e,t){const i=this._terminal,s=i.registerMarker(-i.buffer.active.baseY-i.buffer.active.cursorY+e.row);if(!s)return;const o=i.registerDecoration({marker:s,x:e.col,width:e.size,backgroundColor:t.matchBackground,overviewRulerOptions:this._highlightedLines.has(s.line)?void 0:{color:t.matchOverviewRuler,position:"center"}});if(o){const e=[];e.push(s),e.push(o.onRender((e=>this._applyStyles(e,t.matchBorder,!1)))),e.push(o.onDispose((()=>(0,r.disposeArray)(e))))}return o}}e.SearchAddon=n})(),s})()));
2
+ //# sourceMappingURL=addon-search.js.map
b/481ecdef6a6186997d3771adbe807393ed61d8af62e4897f5cf9314abc447c50 ADDED
The diff for this file is too large to render. See raw diff
 
b/4c3cfb4598607c4de31a9a6bfe1b2de3ecac4a3cc9d347449913ae3c22299fe6 ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$comment": "ΞΊ-pins for the Holo Linux boot. Each artifact is content-addressed; the worker re-derives sha256 and refuses to boot on mismatch (Law L5). The kernel ΞΊ EQUALS the canonical substrate pin at /boot/kernel.uor.json.",
3
+ "algo": "sha256",
4
+ "kernel": {
5
+ "url": "./os-kernel.gz",
6
+ "name": "Linux 6.6 riscv64 Image (gzip)",
7
+ "did": "did:holo:sha256:a7bb1f02a5ac96371ecb402645d25e1cc7cda18c5280f0828f0e31c4fb16162e",
8
+ "sha256": "a7bb1f02a5ac96371ecb402645d25e1cc7cda18c5280f0828f0e31c4fb16162e",
9
+ "ipfs": "bafybeienhapahps7eb5dwnxilfmvmcnjktc6shyy36xcaeaejzrprd6ov4",
10
+ "digestSRI": "sha256-p7sfAqWsljcey0AmRdJeHMfNoYxSgPCCjw4xxPsWFi4=",
11
+ "digestMultibase": "uEiCnux8CpayWNx7LQCZF0l4cx82hjFKA8IKPDjHE-xYWLg",
12
+ "bytes": 6282087,
13
+ "canonicalPin": "/boot/kernel.uor.json",
14
+ "canonicalMatch": true
15
+ },
16
+ "rootfs": {
17
+ "url": "./os-rootfs.tar.gz",
18
+ "name": "Debian 13 (trixie) riscv64 rootfs + Holo Linux overlay (OCI layer, tar+gzip)",
19
+ "did": "did:holo:sha256:8f95f671a596bcbd88e115f388c71df95d2552fb558aed4bebe0234eacca2c0d",
20
+ "sha256": "8f95f671a596bcbd88e115f388c71df95d2552fb558aed4bebe0234eacca2c0d",
21
+ "ipfs": "bafybeih23wx7jq46qfrjeh5ccntkg3jnvznkwsmr55fibfkcwlacz6senu",
22
+ "digestSRI": "sha256-j5X2caWWvL2I4RXziMcd+V0lUvtViu1L6+AjTqzKLA0=",
23
+ "digestMultibase": "uEiCPlfZxpZa8vYjhFfOIxx35XSVS-1WK7Uvr4CNOrMosDQ",
24
+ "bytes": 28344286
25
+ }
26
+ }
b/69e9bfc55ac61137f83090028126b4484b19bd3075a74e9cd0c77aaf1e489bc3 ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // holo-splash.js β€” the Hologram "streaming" boot screen, shared by EVERY Holo app.
2
+ //
3
+ // <holo-splash app="Holo Linux"></holo-splash>
4
+ //
5
+ // One source of truth for the boot experience: every app streams, ΞΊ-verified, like a real hologram β€”
6
+ // same motion, same type, same golden-ratio composition. But the boot screen is now SPECIFIC to each
7
+ // app: it materialises that app's OWN icon (its colocated ./icon.svg), painted in the app's own accent
8
+ // colour, over the shared dark holo-field β€” so launching any app is unmistakably THAT app coming up,
9
+ // while the "Powered by HOLOGRAM" footer keeps the family mark. The only required per-app input is
10
+ // app="…"; the icon and accent are derived automatically (no per-app code), with sane fallbacks so it
11
+ // looks impeccable even for an app that ships no icon.
12
+ //
13
+ // Self-contained custom element: Shadow DOM (styles never leak or clash), zero dependencies, no web
14
+ // fonts (system stack), offline-safe β€” a single file each app vendors. Shows on connect; its progress
15
+ // bar tracks REAL work, so the splash lasts exactly as long as that app's boot actually takes. Drive it:
16
+ // β€’ default (no attribute) β€” AUTO: tracks the page's own load lifecycle (parsing β†’ DOMContentLoaded β†’
17
+ // window 'load' β†’ network settle) and completes once the app is really up. Zero host code.
18
+ // β€’ manual β€” the host drives progress(0..1) through boot milestones and calls complete() when
19
+ // streaming starts. For apps that keep booting past page-load (a VM, a model). A gentle trickle
20
+ // keeps the bar moving between milestones.
21
+ // β€’ duration="ms" β€” a fixed timed fill (generic template / demo).
22
+ // Then it fades through black, removes itself, and fires a bubbling "done" event.
23
+ //
24
+ // PER-APP IDENTITY (override-able, but never required):
25
+ // β€’ icon β€” by default the element fetches "./icon.svg" (each app ships one, colocated). Pass
26
+ // icon="path.svg" to point elsewhere. If the icon can't load, the canonical Hologram mark
27
+ // materialises instead β€” so the screen is always composed.
28
+ // β€’ accent β€” the glow/bar colour. Pass accent="#rrggbb" (any CSS colour) or hue="222" to set it; with
29
+ // neither, a stable, well-distributed hue is derived from the app name, so each app keeps
30
+ // one consistent signature colour across every boot.
31
+ //
32
+ // Golden ratio (Ο† = 1.618) governs every size and position; ONE type unit (--u) governs every text
33
+ // node, so the "Streaming …" label and the "Powered by HOLOGRAM" footer are identical in family and
34
+ // size (only weight/letter-spacing differ, for emphasis). The mark Β· label Β· bar form ONE centred flex
35
+ // column and can NEVER overlap on any screen; the mark is bounded by BOTH axes (vmin AND vh) so on a
36
+ // short-and-wide or tall-and-narrow viewport it shrinks first, always leaving the label, bar and footer
37
+ // their room.
38
+
39
+ // The canonical Hologram mark (boot/boot/icons/os_hologram.svg), inlined so the element is
40
+ // self-contained and boots offline β€” the materialise-fallback when an app ships no loadable icon.
41
+ const MARK =
42
+ '<svg viewBox="-104 -104 208 208" fill="currentColor" role="img" aria-label="Hologram"><g><circle cx="0.20" cy="-97.39" r="2.61"/><circle cx="-22.86" cy="-86.55" r="2.71"/><circle cx="22.54" cy="-86.32" r="2.81"/><circle cx="-0.03" cy="-76.01" r="2.71"/><circle cx="45.26" cy="-75.92" r="7.80"/><circle cx="-45.82" cy="-75.86" r="2.61"/><circle cx="68.34" cy="-65.13" r="7.70"/><circle cx="-68.83" cy="-65.00" r="2.61"/><circle cx="-22.91" cy="-64.90" r="2.61"/><circle cx="22.71" cy="-64.88" r="2.51"/><circle cx="91.24" cy="-54.34" r="2.61"/><circle cx="-45.94" cy="-54.25" r="7.83"/><circle cx="-91.17" cy="-54.19" r="2.71"/><circle cx="-0.03" cy="-54.19" r="2.71"/><circle cx="45.35" cy="-54.19" r="7.80"/><circle cx="-22.86" cy="-43.64" r="2.71"/><circle cx="22.71" cy="-43.49" r="2.51"/><circle cx="68.29" cy="-43.47" r="7.73"/><circle cx="-68.60" cy="-43.37" r="7.73"/><circle cx="-45.85" cy="-32.63" r="7.77"/><circle cx="45.36" cy="-32.60" r="7.80"/><circle cx="-91.26" cy="-32.55" r="2.71"/><circle cx="0.10" cy="-32.51" r="2.61"/><circle cx="91.24" cy="-32.51" r="2.61"/><circle cx="68.22" cy="-21.95" r="7.83"/><circle cx="22.67" cy="-21.84" r="7.87"/><circle cx="-22.86" cy="-21.82" r="2.71"/><circle cx="-68.57" cy="-21.80" r="7.80"/><circle cx="45.45" cy="-11.06" r="7.73"/><circle cx="-0.19" cy="-11.04" r="7.87"/><circle cx="91.35" cy="-11.01" r="2.81"/><circle cx="-91.54" cy="-10.97" r="2.51"/><circle cx="-45.87" cy="-10.87" r="7.73"/><circle cx="22.71" cy="-0.27" r="8.06"/><circle cx="-22.89" cy="-0.21" r="7.90"/><circle cx="68.28" cy="-0.15" r="7.87"/><circle cx="-68.62" cy="-0.11" r="8.00"/><circle cx="-0.06" cy="10.98" r="7.87"/><circle cx="45.54" cy="11.00" r="7.83"/><circle cx="-45.85" cy="11.02" r="7.77"/><circle cx="-91.26" cy="11.10" r="2.71"/><circle cx="91.24" cy="11.13" r="2.61"/><circle cx="22.71" cy="21.64" r="2.71"/><circle cx="-68.74" cy="21.66" r="7.87"/><circle cx="-22.86" cy="21.67" r="7.87"/><circle cx="68.28" cy="21.67" r="7.87"/><circle cx="-91.54" cy="32.46" r="2.61"/><circle cx="0.15" cy="32.46" r="2.71"/><circle cx="-45.72" cy="32.50" r="7.73"/><circle cx="45.54" cy="32.59" r="7.83"/><circle cx="91.35" cy="32.63" r="2.81"/><circle cx="-23.01" cy="43.23" r="2.61"/><circle cx="68.25" cy="43.31" r="7.83"/><circle cx="-68.71" cy="43.34" r="7.83"/><circle cx="22.71" cy="43.37" r="2.90"/><circle cx="91.39" cy="53.92" r="2.71"/><circle cx="45.48" cy="53.95" r="7.87"/><circle cx="-45.86" cy="53.97" r="7.80"/><circle cx="-91.34" cy="53.99" r="2.61"/><circle cx="0.20" cy="54.09" r="2.61"/><circle cx="-68.57" cy="64.90" r="7.80"/><circle cx="-22.86" cy="64.92" r="2.71"/><circle cx="68.28" cy="64.92" r="2.71"/><circle cx="22.54" cy="65.15" r="2.81"/><circle cx="-45.88" cy="75.56" r="7.80"/><circle cx="0.10" cy="75.62" r="2.61"/><circle cx="45.32" cy="75.62" r="2.61"/><circle cx="22.53" cy="86.47" r="2.71"/><circle cx="-22.86" cy="86.75" r="2.71"/><circle cx="-0.03" cy="97.29" r="2.71"/></g></svg>';
43
+
44
+ const TEMPLATE = `
45
+ <style>
46
+ /* ── golden-ratio composition ─────────────────────────────────────────────────
47
+ Ο† = 1.618. ONE type unit --u drives every text node; --g1/--g2/--g3 are φ¹/φ²/φ³
48
+ of it for vertical rhythm. The mark Β· label Β· bar form ONE centred flex column, so
49
+ they scale as a unit and can NEVER overlap on any screen or aspect ratio. The stack's
50
+ optical centre rides the upper golden line (38.2%): it lives in the top band (height
51
+ 2 Γ— 38.2% = 76.4%), leaving the lower golden band (23.6%) to breathe and carry the
52
+ footer. Every size is Ο†-derived and bounded by the SHORT axis (vmin) so nothing ever
53
+ outgrows the frame β€” portrait phone, ultra-wide, or short laptop all stay composed. */
54
+ :host {
55
+ position: fixed; inset: 0; z-index: 2147483000; display: block; overflow: hidden;
56
+ --phi: 1.618;
57
+ --u: clamp(15px, 1.7vmin, 21px); /* the ONE type size β€” short-axis-bounded */
58
+ --g1: calc(var(--u) * 1.618); /* φ¹ */
59
+ --g2: calc(var(--u) * 2.618); /* φ² */
60
+ --g3: calc(var(--u) * 4.236); /* φ³ */
61
+ --teal: #7defc9; /* the family mark colour (footer) */
62
+ --accent: #7defc9; /* the per-app signature colour β€” set from JS */
63
+ font-family: "Segoe UI", system-ui, -apple-system, "Oxygen", sans-serif; /* the ONE family */
64
+ color: #fff;
65
+ background: radial-gradient(120% 120% at 20% 0%, #1b2a4a 0%, #0d1117 60%, #05070c 100%);
66
+ opacity: 1; transition: opacity 0.55s ease; /* the shared black-veil fade-out */
67
+ }
68
+ :host(.gone) { opacity: 0; pointer-events: none; }
69
+ /* aurora β€” the same wallpaper the OS greeter wears, screen-blended over the black base. Its third
70
+ bloom takes the app's accent, so even the ambient field is tinted to the app coming up. */
71
+ .aurora { position: absolute; inset: 0; mix-blend-mode: screen; pointer-events: none; }
72
+ .aurora i { position: absolute; border-radius: 50%; filter: blur(60px); opacity: 0.5; }
73
+ .aurora .a { left: 8%; top: 12%; width: 42vw; height: 42vw; background: radial-gradient(circle, rgba(52,211,166,0.5), transparent 62%); }
74
+ .aurora .b { right: 6%; bottom: 4%; width: 46vw; height: 46vw; background: radial-gradient(circle, rgba(64,99,214,0.45), transparent 62%); }
75
+ .aurora .c { left: 44%; top: 48%; width: 30vw; height: 30vw;
76
+ background: radial-gradient(circle, color-mix(in srgb, var(--accent) 30%, transparent), transparent 62%); }
77
+ /* the centred stack β€” fills the upper band (bottom: 23.6%), so its midpoint sits on the
78
+ upper golden line (38.2%). align/justify-center keep markΒ·labelΒ·bar a single composed unit. */
79
+ .stage { position: absolute; left: 0; right: 0; top: 0; bottom: 23.6%;
80
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
81
+ padding: var(--g2) var(--g3); box-sizing: border-box; text-align: center; }
82
+ /* the breathing mark frame β€” a fixed-size square (so the icon's arrival causes NO layout shift),
83
+ bounded by the short axis AND the viewport height (40vh) so on a short/wide or tall/narrow screen
84
+ it shrinks first, always leaving the label Β· bar Β· footer their room: the no-overlap guarantee. */
85
+ .mark { position: relative; width: min(31vmin, 16rem, 40vh); aspect-ratio: 1; flex: 0 0 auto;
86
+ color: var(--accent); display: grid; place-items: center;
87
+ animation: breathe 2.618s ease-in-out infinite; } /* φ²-second cadence */
88
+ /* a soft accent halo, present from the first frame, so the mark area reads as "materialising" even
89
+ before the icon resolves β€” and gives the glyph its hologram glow once it does. */
90
+ .mark::before { content: ""; position: absolute; inset: -8%; border-radius: 50%; z-index: 0;
91
+ background: radial-gradient(circle, color-mix(in srgb, var(--accent) 34%, transparent) 0%, transparent 66%);
92
+ filter: blur(6px); opacity: 0.9; }
93
+ /* the glyph itself β€” the app's icon (or the Hologram mark fallback). Fades + lifts in once loaded. */
94
+ .glyph { position: relative; z-index: 1; width: 86%; height: 86%; display: grid; place-items: center;
95
+ opacity: 0; transform: scale(0.94); transition: opacity 0.5s ease, transform 0.5s ease;
96
+ filter: drop-shadow(0 0 16px color-mix(in srgb, var(--accent) 50%, transparent))
97
+ drop-shadow(0 0 40px color-mix(in srgb, var(--accent) 26%, transparent)); }
98
+ .mark.ready .glyph { opacity: 1; transform: scale(1); }
99
+ .glyph svg { width: 100%; height: 100%; display: block; }
100
+ /* the Hologram-mark fallback wants the full frame (it has no built-in padding like the app icons) */
101
+ .glyph.hmark { width: 100%; height: 100%; }
102
+ @keyframes breathe { 0%, 100% { transform: scale(1); opacity: 0.92; }
103
+ 50% { transform: scale(1.04); opacity: 1; } }
104
+ /* label β€” φ² below the mark, φ¹ above the bar (the two gaps are themselves in golden ratio). It
105
+ prefers one line, but a long app name on a narrow screen WRAPS (centred, balanced) rather than
106
+ overflowing the frame β€” so the composition stays contained for any app name. */
107
+ .label { margin: var(--g2) 0 var(--g1); font-size: var(--u); font-weight: 400; line-height: 1.25;
108
+ letter-spacing: 0.06em; max-width: 90vw; text-wrap: balance; color: rgba(255,255,255,0.82); }
109
+ .label b { font-weight: 600; color: #fff; letter-spacing: 0.12em; }
110
+ /* ONE progress bar β€” Ο†-derived width, bounded so it never spans an ultra-wide screen. The fill takes
111
+ the app accent, so even the progress reads in the app's colour. */
112
+ .bar { width: min(38.2vw, 30rem); height: 2px; border-radius: 2px; flex: 0 0 auto;
113
+ background: rgba(255,255,255,0.13); overflow: hidden; }
114
+ .fill { height: 100%; width: 0; border-radius: 2px;
115
+ background: color-mix(in srgb, var(--accent) 85%, #fff);
116
+ box-shadow: 0 0 10px color-mix(in srgb, var(--accent) 60%, transparent);
117
+ transition: width 0.3s cubic-bezier(0.3, 0, 0.25, 1); }
118
+ /* footer β€” φ² inset from the bottom, in the lower golden band. font-size: var(--u) (= the label). */
119
+ .foot { position: absolute; left: 50%; bottom: var(--g2); transform: translateX(-50%);
120
+ font-size: var(--u); font-weight: 400; line-height: 1; letter-spacing: 0.06em; white-space: nowrap;
121
+ color: rgba(231,237,250,0.5); }
122
+ .foot b { font-weight: 600; color: var(--teal); letter-spacing: 0.14em; }
123
+ @media (prefers-reduced-motion: reduce) { .mark { animation: none; }
124
+ .glyph { transition: opacity 0.3s ease; transform: none; } .mark.ready .glyph { transform: none; } }
125
+ /* ── compact mode for SHORT viewports ─────────────────────────────────────────────────────────
126
+ The golden composition is sized for normal aspect ratios; on a short surface (landscape phone, a
127
+ stubby window) the absolute footer would eventually crowd the bar. So below 480px tall we tighten
128
+ the whole rhythm (a smaller --u shrinks every gap with it, and the mark gains a tighter vh bound),
129
+ and below 300px tall β€” where there is simply no room to seat the brand line without crowding β€” we
130
+ drop the footer. Together these GUARANTEE the mark Β· label Β· bar Β· footer never overlap at any size. */
131
+ @media (max-height: 480px) {
132
+ :host { --u: clamp(11px, 3vmin, 18px); }
133
+ .mark { width: min(31vmin, 16rem, 36vh); }
134
+ }
135
+ @media (max-height: 300px) { .foot { display: none; } }
136
+ </style>
137
+ <div class="aurora" aria-hidden="true"><i class="a"></i><i class="b"></i><i class="c"></i></div>
138
+ <div class="stage">
139
+ <div class="mark" aria-hidden="true"><div class="glyph"></div></div>
140
+ <div class="label">Streaming <b class="app"></b></div>
141
+ <div class="bar"><div class="fill"></div></div>
142
+ </div>
143
+ <div class="foot">Powered by <b>HOLOGRAM</b></div>
144
+ `;
145
+
146
+ class HoloSplash extends HTMLElement {
147
+ connectedCallback() {
148
+ if (this._init) return;
149
+ this._init = true;
150
+ const root = this.attachShadow({ mode: "open" });
151
+ root.innerHTML = TEMPLATE;
152
+ const app = this.getAttribute("app") || "";
153
+ root.querySelector(".app").textContent = app;
154
+ this._mark = root.querySelector(".mark");
155
+ this._glyph = root.querySelector(".glyph");
156
+ this._fill = root.querySelector(".fill");
157
+ this._shown = 0; // visible bar fraction
158
+ this._milestone = 0; // highest real milestone reported
159
+ this._cap = 0; // trickle ceiling (a little above the milestone)
160
+
161
+ // ── per-app accent ───────────────────────────────────────────────────────────────────────
162
+ // An explicit accent="…"/hue="…" wins; otherwise derive a stable, well-spread hue from the app
163
+ // name so each app keeps ONE signature colour across every boot. Fixed S/L keep it luminous on
164
+ // the dark field, never muddy.
165
+ this.style.setProperty("--accent", this._accent(app));
166
+ // ── per-app glyph ────────────────────────────────────────────────────────────────────────
167
+ // Materialise the app's own icon; fall back to the Hologram mark if it can't load.
168
+ this._loadGlyph();
169
+
170
+ const dur = +this.getAttribute("duration");
171
+ if (this.hasAttribute("manual")) {
172
+ // Manual mode: the host drives progress() to real boot milestones and calls complete() when
173
+ // streaming begins β€” for apps whose boot runs well past page-load (a VM, a model). A gentle
174
+ // trickle keeps the bar moving between milestones (and through long synchronous work like
175
+ // ΞΊ-disk provisioning), so the bar always reflects the actual time taken.
176
+ this._iv = setInterval(() => this._trickle(), 200);
177
+ this._safety = setTimeout(() => this.complete(), 120000); // never trap the user behind the splash
178
+ } else if (dur > 0) {
179
+ // Timed mode: no boot to track (generic template / demo) β€” fill smoothly over `duration`,
180
+ // then finish. The first progress()/complete() call switches it to live mode.
181
+ requestAnimationFrame(() => {
182
+ if (this._driven || !this._fill) return;
183
+ this._fill.style.transition = "width " + dur + "ms cubic-bezier(0.3, 0, 0.25, 1)";
184
+ this._fill.style.width = "100%";
185
+ });
186
+ this._timer = setTimeout(() => { if (!this._driven) this.complete(); }, dur);
187
+ } else {
188
+ // Auto mode (the default for every app): no host code needed β€” the splash tracks THIS app's
189
+ // own real boot, so its lifetime reflects the actual time the app takes to come up. It lifts
190
+ // the bar across the document/resource load milestones (parsing β†’ DOMContentLoaded β†’ load) and
191
+ // finishes a moment after the page is up and the network goes quiet. A host may still override
192
+ // at any time with progress()/complete(). Apps that keep booting past page-load use the
193
+ // `manual` attribute and drive the milestones themselves.
194
+ this._cap = 0.2;
195
+ this._iv = setInterval(() => this._trickle(), 200);
196
+ this._safety = setTimeout(() => this.complete(), 120000);
197
+ this._autoBoot();
198
+ }
199
+ }
200
+
201
+ // Resolve the app's signature accent colour. Explicit attribute wins; else a stable hue from the name.
202
+ _accent(app) {
203
+ const a = (this.getAttribute("accent") || "").trim();
204
+ if (a) return a;
205
+ const hAttr = this.getAttribute("hue");
206
+ let hue;
207
+ if (hAttr != null && hAttr !== "" && isFinite(+hAttr)) hue = ((+hAttr % 360) + 360) % 360;
208
+ else {
209
+ // FNV-style hash β†’ golden-angle spread for maximally distinct neighbours.
210
+ let h = 2166136261 >>> 0;
211
+ const s = app || "Hologram";
212
+ for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619) >>> 0; }
213
+ hue = Math.round(((h % 1000) / 1000) * 360 + (s.length * 137.508)) % 360;
214
+ }
215
+ return `hsl(${hue} 70% 66%)`;
216
+ }
217
+
218
+ // Fetch and inline the app's own icon (./icon.svg by default), painted in the accent via currentColor.
219
+ // Same-origin, tiny, cached. On any failure (no icon, off-http, parse error) the Hologram mark
220
+ // materialises instead β€” the screen is always composed. The fade-in is reserved-space, so the icon's
221
+ // arrival never shifts the layout.
222
+ _loadGlyph() {
223
+ const src = this.getAttribute("icon") || "./icon.svg";
224
+ const fallback = () => this._setGlyph(MARK, true);
225
+ let settled = false;
226
+ const guard = setTimeout(() => { if (!settled) { settled = true; fallback(); } }, 1500); // never wait on a slow/missing icon
227
+ fetch(src, { cache: "force-cache" })
228
+ .then(r => (r.ok && /svg|xml/.test(r.headers.get("content-type") || "") ? r.text()
229
+ : r.ok ? r.text() : Promise.reject()))
230
+ .then(txt => {
231
+ if (settled) return; settled = true; clearTimeout(guard);
232
+ const svg = this._extractSvg(txt);
233
+ if (svg) this._setGlyph(svg, false); else fallback();
234
+ })
235
+ .catch(() => { if (settled) return; settled = true; clearTimeout(guard); fallback(); });
236
+ }
237
+ // Pull just the <svg>…</svg> out of the file and confirm it parses β€” never inject arbitrary markup.
238
+ _extractSvg(txt) {
239
+ try {
240
+ const doc = new DOMParser().parseFromString(String(txt), "image/svg+xml");
241
+ const svg = doc.querySelector("svg");
242
+ if (!svg || doc.querySelector("parsererror")) return null;
243
+ // make it fill its frame regardless of authored width/height
244
+ svg.removeAttribute("width"); svg.removeAttribute("height");
245
+ svg.setAttribute("aria-hidden", "true");
246
+ return svg.outerHTML;
247
+ } catch (_) { return null; }
248
+ }
249
+ _setGlyph(svgHtml, isHmark) {
250
+ if (!this._glyph || this._done) return;
251
+ this._glyph.innerHTML = svgHtml;
252
+ this._glyph.classList.toggle("hmark", !!isHmark);
253
+ // trigger the fade/scale-in once initial styles have applied. A timer (not just rAF) so the reveal
254
+ // still fires when the tab is backgrounded β€” rAF is throttled there; setTimeout is not.
255
+ const reveal = () => { if (this._mark) this._mark.classList.add("ready"); };
256
+ requestAnimationFrame(reveal);
257
+ setTimeout(reveal, 60);
258
+ }
259
+
260
+ // Drive the bar from the page's own load lifecycle so the splash lasts as long as the app's real
261
+ // boot. Lifts to milestones at DOMContentLoaded and window 'load', then completes once the on-load
262
+ // network burst settles (bounded, so a continuously-streaming app is never trapped behind it).
263
+ _autoBoot() {
264
+ const lift = (m, cap) => {
265
+ if (this._done || this._completing || this._driven) return;
266
+ this._milestone = Math.max(this._milestone, m);
267
+ if (this._shown < m) this._shown = m;
268
+ this._cap = Math.max(this._cap, cap);
269
+ this._apply();
270
+ };
271
+ if (document.readyState === "loading")
272
+ document.addEventListener("DOMContentLoaded", () => lift(0.55, 0.9), { once: true });
273
+ else lift(0.55, 0.9);
274
+
275
+ const settle = () => {
276
+ lift(0.9, 0.97);
277
+ if (this._done || this._completing) return;
278
+ const QUIET = 500, MAX = 1500; // finish 0.5s after the network quiets, but never wait > 1.5s
279
+ let quiet = null;
280
+ const finish = () => { try { this._po && this._po.disconnect(); } catch (_) {} this._po = null;
281
+ clearTimeout(quiet); clearTimeout(hard); this.complete(); };
282
+ const arm = () => { clearTimeout(quiet); quiet = setTimeout(finish, QUIET); };
283
+ const hard = setTimeout(finish, MAX);
284
+ try { this._po = new PerformanceObserver(() => arm()); this._po.observe({ type: "resource", buffered: false }); } catch (_) {}
285
+ arm();
286
+ };
287
+ if (document.readyState === "complete") settle();
288
+ else window.addEventListener("load", settle, { once: true });
289
+ }
290
+ // Report a real milestone in [0,1). Monotonic; lifts the trickle ceiling a little above it.
291
+ progress(f) {
292
+ this._driven = true;
293
+ if (this._timer) { clearTimeout(this._timer); this._timer = null; }
294
+ f = Math.max(0, Math.min(0.999, +f || 0));
295
+ this._milestone = Math.max(this._milestone, f);
296
+ this._cap = Math.min(0.97, this._milestone + 0.1);
297
+ if (this._shown < this._milestone) this._shown = this._milestone;
298
+ this._apply();
299
+ }
300
+ _trickle() {
301
+ if (this._done) return;
302
+ if (this._shown < this._cap) { this._shown += (this._cap - this._shown) * 0.08; this._apply(); }
303
+ }
304
+ _apply() { if (this._fill) this._fill.style.width = (Math.min(this._shown, 0.999) * 100).toFixed(2) + "%"; }
305
+ // Boot reached the streaming point: fill to 100%, then fade out.
306
+ complete() {
307
+ if (this._completing || this._done) return;
308
+ this._completing = true; this._driven = true;
309
+ clearInterval(this._iv); clearTimeout(this._timer); clearTimeout(this._safety);
310
+ try { this._po && this._po.disconnect(); } catch (_) {} this._po = null;
311
+ this._shown = 1;
312
+ if (this._fill) { this._fill.style.transition = "width 0.35s ease-out"; this._fill.style.width = "100%"; }
313
+ setTimeout(() => this.dismiss(), 380); // let the bar visibly complete before fading
314
+ }
315
+ // Get out of the way immediately (e.g. on a boot error) so what's underneath shows.
316
+ dismiss() {
317
+ if (this._done) return;
318
+ this._done = true;
319
+ clearInterval(this._iv); clearTimeout(this._timer); clearTimeout(this._safety);
320
+ const done = () => { this.dispatchEvent(new CustomEvent("done", { bubbles: true })); this.remove(); };
321
+ this.addEventListener("transitionend", (e) => { if (e.propertyName === "opacity") done(); }, { once: true });
322
+ setTimeout(done, 700); // fallback if transitionend doesn't fire (> the 0.55s fade)
323
+ this.classList.add("gone");
324
+ }
325
+ disconnectedCallback() { clearInterval(this._iv); clearTimeout(this._timer); clearTimeout(this._safety);
326
+ try { this._po && this._po.disconnect(); } catch (_) {} this._po = null; }
327
+ }
328
+
329
+ if (!customElements.get("holo-splash")) customElements.define("holo-splash", HoloSplash);
b/78381834c93186218a1c3be259d514cfbf151e40b99b781ea9feb79e371a99c5 ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # serverless β€” there is no server. This entire Linux runs inside your browser tab.
3
+ # Everything below is read LIVE from the running kernel; nothing here is fetched over a wire.
4
+
5
+ A=$'\033[1;38;5;51m'; D=$'\033[38;5;244m'; G=$'\033[38;5;72m'; R=$'\033[0m'
6
+
7
+ printf '\n%sHolo Linux is serverless%s\n\n' "$A" "$R"
8
+
9
+ printf ' %sno network stack%s ' "$G" "$R"
10
+ net=""; for c in ip ifconfig ping curl wget ssh nc; do command -v "$c" >/dev/null 2>&1 && net="$net $c"; done
11
+ if [ -z "$net" ]; then printf 'ip Β· ping Β· curl Β· wget Β· ssh Β· nc β€” none exist; nothing can phone home\n'
12
+ else printf 'present:%s\n' "$net"; fi
13
+
14
+ printf ' %sinit Β· PID 1%s %s ' "$G" "$R" "$(cat /proc/1/comm 2>/dev/null)"
15
+ printf '%s(a real init the kernel started β€” not a server reply)%s\n' "$D" "$R"
16
+
17
+ printf ' %salive%s %ss of uptime, entirely inside this tab\n' "$G" "$R" "$(awk '{printf "%.0f", $1}' /proc/uptime 2>/dev/null)"
18
+
19
+ printf '\n%sNo server booted this kernel. No request served this shell. The CPU, the\n' "$D"
20
+ printf 'filesystem, every syscall β€” all execute on an ISA core in the page itself.\n'
21
+ printf 'Turn your network off and keep typing: it just keeps running (try ✈ offline boot).%s\n\n' "$R"
b/78ec7022aff858c52db5fda0a79a03b757d9d2b186e3c25ce3357ada0319985d ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # reproduce β€” this machine is defined by content, not by a server. Every file in the rootfs is
3
+ # content-addressed: the same bytes give the same ΞΊ on every boot, on any device. The whole
4
+ # kernel + rootfs are pinned by ΞΊ in kappa.json and re-derived at boot (the βœ“ ΞΊ-anchored rail
5
+ # above) β€” so a few hex characters reproduce the identical OS, byte-for-byte, anywhere, forever.
6
+
7
+ A=$'\033[1;38;5;51m'; D=$'\033[38;5;244m'; B=$'\033[38;5;39m'; R=$'\033[0m'
8
+
9
+ printf '\n%sHolo Linux is reproducible%s\n\n' "$A" "$R"
10
+
11
+ printf '%sSame bytes β†’ the same ΞΊ, on every boot and on any machine. A few derived live now:%s\n\n' "$D" "$R"
12
+ sha256sum /etc/os-release /etc/profile.d/00-holo-linux.sh /usr/local/bin/verify 2>/dev/null | sed 's/^/ /'
13
+
14
+ printf '\n%sThe whole machine β€” this exact kernel + rootfs β€” is pinned by ΞΊ in kappa.json and\n' "$D"
15
+ printf 're-derived at boot (see the βœ“ ΞΊ-anchored rail above the terminal). Hand someone\n'
16
+ printf 'that ΞΊ and they rebuild this OS byte-for-byte β€” no server, no trust, no drift.%s\n\n' "$R"
b/816d2f31ed1feb6e4d18e69c6bd81cdf9d1cc63fd650680b9f4ed83867cd52c6 ADDED
@@ -0,0 +1,696 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en" data-holo-kappa-auto="off" data-holo-boot="off">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <script>
6
+ // ΞΊ-MACHINE ORIGIN bounce. This app's writable ΞΊ-disk uses OPFS createSyncAccessHandle, which FAILS on the
7
+ // holo:// custom scheme ("…files are unsafe… too many calls on file resources") β€” that API is http/https-only.
8
+ // The native host runs a loopback static server (broker_server.cc, :8495) that serves this app + the wasm
9
+ // engine WITH COOP/COEP, so both OPFS and SharedArrayBuffer (crossOriginIsolated) work. On holo://, bounce
10
+ // there (query params preserved). Must run FIRST, before any asset loads.
11
+ // NOTE: localhost, NOT 127.0.0.1 β€” the WebAuthn spec forbids IP literals as RP IDs (http://127.0.0.1 throws
12
+ // SecurityError; http://localhost is a valid secure-context origin), matching the host's stepup-broker URL.
13
+ if (location.protocol === "holo:") {
14
+ location.replace("http://localhost:8495/apps/holo-linux/index.html" + location.search);
15
+ }
16
+ </script>
17
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
18
+ <title>Holo Linux</title>
19
+ <!-- HoloFX motion engine (data-holo-spin driver); kappa-auto/boot off to avoid touching the emulator surface -->
20
+ <script src="../../_shared/holo-fx.js"></script>
21
+ <!-- Holo Linux service worker. Two jobs: (1) stamp COOP/COEP so the engine's
22
+ SharedArrayBuffer works on any static host (no server headers needed), and
23
+ (2) cache the whole app β€” shell, engine wasm, kernel + rootfs β€” so once
24
+ loaded it boots with the server OFF. 100% serverless after first load.
25
+ All app assets are vendored under this scope so the SW can serve every byte. -->
26
+ <script>
27
+ // The SW only exists to stamp COOP/COEP on dumb static hosts. Natively (holo://) the ΞΊ-route already
28
+ // sends them on every response (cross-origin isolation is intrinsic), so we SKIP the SW there β€” it would
29
+ // otherwise intercept ΞΊ-route fetches. On http(s) static hosts we still register it for SAB.
30
+ if ("serviceWorker" in navigator && location.protocol !== "holo:") {
31
+ navigator.serviceWorker.register("./holo-linux-sw.js", { scope: "./" }).catch(() => {});
32
+ // On a header-less static host the FIRST load isn't isolated until the SW
33
+ // controls the page; reload once so SharedArrayBuffer becomes available.
34
+ if (!self.crossOriginIsolated && !sessionStorage.getItem("holoCoiReload")) {
35
+ navigator.serviceWorker.ready.then(() => { sessionStorage.setItem("holoCoiReload", "1"); location.reload(); });
36
+ }
37
+ }
38
+ </script>
39
+ <link rel="stylesheet" href="./vendor/xterm/xterm.css" />
40
+ <style>
41
+ /* Dark, real-CLI. Windows-Terminal "Campbell" palette: near-black surface,
42
+ soft-white text, authentic ANSI β€” no neon. Chrome is a slim dark tab bar
43
+ + a status-line ΞΊ rail, so it reads as a terminal window, not a web app. */
44
+ :root {
45
+ color-scheme: dark;
46
+ --bg: #0C0C0C; /* terminal surface */
47
+ --chrome: #1A1A1A; /* slim tab/title bar */
48
+ --fg: #CCCCCC; /* soft white */
49
+ --dim: #9AA0A8; /* secondary text β€” readable on the dark bar (β‰ˆ6:1) */
50
+ --faint: #868D96; /* tertiary / counters β€” still clearly visible (β‰ˆ4.6:1) */
51
+ --line: #2A2A2A; /* hairline dividers */
52
+ --acc: #3B78FF; /* blue */
53
+ --ok: #16C60C; /* green */
54
+ --bad: #E74856; /* red */
55
+ --warn: #CCA700; /* amber */
56
+ --hover: #2A2A2A;
57
+ --code: #1E1E1E; /* inline code chip bg */
58
+ }
59
+ * { box-sizing: border-box; }
60
+ html, body { height: 100%; margin: 0; overflow: hidden; } /* never scroll the page; the terminal owns scrolling */
61
+ body { background: var(--bg); color: var(--fg); display: flex; flex-direction: column;
62
+ font: 12.5px/1.45 "Cascadia Mono", "Cascadia Code", Consolas, "JetBrains Mono", ui-monospace, Menlo, monospace;
63
+ -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
64
+
65
+ /* Slim dark tab/title bar: identity Β· live state Β· throughput Β· actions. */
66
+ #bar { flex: 0 0 auto; display: flex; align-items: center; gap: 0.65rem;
67
+ padding: 0.3rem 0.7rem; background: var(--chrome); border-bottom: 1px solid #000; user-select: none; }
68
+ #bar .name { color: #F2F2F2; font-weight: 600; letter-spacing: 0.01em; }
69
+ #bar .tag { color: var(--dim); }
70
+ #bar .sp { margin-left: auto; }
71
+ #bar .stat { color: var(--dim); font-variant-numeric: tabular-nums; }
72
+ #bar .dot { color: var(--warn); } #bar .dot.on { color: var(--ok); } #bar .dot.off { color: var(--bad); }
73
+ #bar button { font: inherit; color: var(--dim); background: transparent;
74
+ border: 1px solid transparent; border-radius: 5px; padding: 0.16rem 0.55rem; cursor: pointer;
75
+ transition: background .12s, color .12s; }
76
+ #bar button:hover { color: #F2F2F2; background: var(--hover); }
77
+ #bar button:active { background: #333333; }
78
+
79
+ /* ΞΊ verification rail β€” reads as a terminal status line; understated, always honest. */
80
+ #kappa { flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: 0.3rem 1.5rem; align-items: center;
81
+ padding: 0.34rem 0.7rem; background: var(--bg); border-bottom: 1px solid var(--line); font-size: 11.5px; }
82
+ #kappa .lbl { color: var(--faint); letter-spacing: 0.01em; }
83
+ .pin { display: flex; align-items: center; gap: 0.45rem; min-width: 0; color: var(--dim); }
84
+ .pin .k { color: var(--acc); word-break: break-all; opacity: 0.85; }
85
+ .pin .v { flex: 0 0 auto; color: var(--warn); font-weight: 600; }
86
+ .pin.ok .v { color: var(--ok); } .pin.bad .v { color: var(--bad); }
87
+ .pin .badge { flex: 0 0 auto; color: var(--ok); border: 1px solid #1F3B25; background: #0E1C12;
88
+ border-radius: 999px; padding: 0.02rem 0.5rem; font-size: 10.5px; }
89
+
90
+ #stage { position: relative; flex: 1 1 auto; min-height: 0; background: var(--bg); overflow: hidden; }
91
+ /* breathing room: even inner margin so the cursor/prompt never touch the
92
+ window edges (the way GNOME Terminal / iTerm keep a gutter around the grid).
93
+ FitAddon derives rows from this padded box, so the bottom gutter is real
94
+ empty space below the last row β€” not clipped output. */
95
+ /* content-box (not the global border-box): FitAddon reads getComputedStyle(#term).height
96
+ and assumes it's the CONTENT height, then sizes rows to fill it. With border-box that
97
+ value includes the padding, so the grid overflows the bottom edge. content-box makes
98
+ the reported height exclude the padding, so the gutter below is real, typable space. */
99
+ #term { position: absolute; inset: 0; padding: 16px 14px 24px 16px; overflow: hidden; box-sizing: content-box; }
100
+ .xterm { height: 100%; }
101
+ /* Vertical scroll is always present (reserves its own gutter, so growing output
102
+ never reflows the grid). Horizontal scroll is off β€” long lines wrap, the way
103
+ GNOME Terminal / iTerm behave. */
104
+ .xterm .xterm-viewport { overflow-y: scroll !important; overflow-x: hidden !important; }
105
+ .xterm-viewport::-webkit-scrollbar { width: 14px; }
106
+ .xterm-viewport::-webkit-scrollbar-track { background: transparent; }
107
+ .xterm-viewport::-webkit-scrollbar-thumb { background: #4A4A4A; border: 3px solid var(--bg); border-radius: var(--holo-radius-sm, 8px); }
108
+ .xterm-viewport::-webkit-scrollbar-thumb:hover { background: #5E5E5E; }
109
+ /* Firefox */
110
+ .xterm .xterm-viewport { scrollbar-width: thin; scrollbar-color: #4A4A4A transparent; }
111
+
112
+ /* Pre-boot status / error panel over the empty terminal. Hides once the guest owns the screen. */
113
+ #overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
114
+ background: var(--bg); transition: opacity 0.25s; z-index: 5; }
115
+ #overlay.hidden { opacity: 0; pointer-events: none; }
116
+ #panel { max-width: 44rem; padding: 1.5rem 1.75rem; }
117
+ #panel .ttl { color: #F2F2F2; font-size: 1.02rem; font-weight: 600; margin-bottom: 0.25rem; }
118
+ #panel .sub { color: var(--dim); margin-bottom: 1.1rem; line-height: 1.5; }
119
+ #panel .status { display: flex; align-items: center; gap: 0.6rem; color: var(--fg); }
120
+ #panel .spin { color: var(--acc); font: 700 13px/1 ui-monospace, Menlo, Consolas, monospace; min-width: 1.1em; display: inline-block; }
121
+ #panel.err .status { color: var(--bad); } #panel.err .spin { display: none; }
122
+ #panel .hint { margin-top: 0.8rem; color: var(--dim); white-space: pre-wrap; line-height: 1.5; }
123
+ #panel code { color: #79C0FF; background: var(--code); padding: 0.05rem 0.35rem; border-radius: 4px; }
124
+
125
+ /* "Verify it yourself" β€” a full-height drawer that slides out from the right,
126
+ over a dimming backdrop. Off-canvas at rest; clipped so it never scrolls the page. */
127
+ #scrim { position: fixed; inset: 0; z-index: 20; background: rgba(0,0,0,0.45);
128
+ opacity: 0; pointer-events: none; transition: opacity 0.22s ease; }
129
+ #scrim.open { opacity: 1; pointer-events: auto; }
130
+ #verify { position: fixed; top: 0; right: 0; bottom: 0; width: min(26rem, 90vw); z-index: 30;
131
+ background: #161616; border-left: 1px solid var(--line); transform: translateX(100%);
132
+ transition: transform 0.22s ease; display: flex; flex-direction: column; box-shadow: -18px 0 50px rgba(0,0,0,0.6); }
133
+ #verify.open { transform: none; }
134
+ #verify h3 { margin: 0; padding: 0.7rem 0.9rem; font-size: 13px; color: #F2F2F2; border-bottom: 1px solid var(--line);
135
+ display: flex; align-items: center; gap: 0.5rem; font-weight: 600; }
136
+ #verify h3 .x { margin-left: auto; cursor: pointer; color: var(--dim); border: 0; background: transparent; font: inherit; }
137
+ #verify h3 .x:hover { color: #F2F2F2; }
138
+ #verify .intro { padding: 0.6rem 0.9rem; color: var(--dim); font-size: 12px; border-bottom: 1px solid var(--line); line-height: 1.5; }
139
+ #verify .list { overflow: auto; padding: 0.4rem 0.6rem 0.8rem; }
140
+ #verify .ghead { color: var(--faint); font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em;
141
+ margin: 0.85rem 0.25rem 0.15rem; }
142
+ #verify .ghead:first-child { margin-top: 0.25rem; }
143
+ .chk { border: 1px solid var(--line); border-radius: 7px; padding: 0.5rem 0.6rem; margin: 0.4rem 0; background: var(--bg); }
144
+ .chk .why { color: var(--dim); font-size: 11.5px; margin-bottom: 0.4rem; line-height: 1.45; }
145
+ .chk .row { display: flex; align-items: center; gap: 0.4rem; }
146
+ .chk code { flex: 1 1 auto; color: #7EE787; background: var(--code); border: 1px solid var(--line);
147
+ border-radius: 5px; padding: 0.26rem 0.45rem; font-size: 12px; overflow-x: auto; white-space: pre; }
148
+ .chk button { flex: 0 0 auto; color: #79C0FF; background: transparent; border: 1px solid #243A5E;
149
+ border-radius: 5px; padding: 0.24rem 0.55rem; cursor: pointer; font: inherit; font-size: 12px; transition: background .12s; }
150
+ .chk button:hover { background: #15233B; }
151
+ #verify .foot { padding: 0.55rem 0.9rem; border-top: 1px solid var(--line); }
152
+ #verify .foot button { width: 100%; color: #0C0C0C; background: var(--ok); border: 0; border-radius: 6px;
153
+ padding: 0.5rem; cursor: pointer; font: inherit; font-weight: 600; transition: filter .12s; }
154
+ #verify .foot button:hover { filter: brightness(1.1); }
155
+
156
+ /* "How it works" β€” a wider right-side reading drawer (same slide-out + scrim as verify),
157
+ but tuned for long-form reading: a centred ~62ch column, readable sans-serif prose,
158
+ monospace only for the clickable command chips. */
159
+ #howto { position: fixed; top: 0; right: 0; bottom: 0; width: min(48rem, 96vw); z-index: 30;
160
+ background: #131313; border-left: 1px solid var(--line); transform: translateX(100%);
161
+ transition: transform 0.22s ease; display: flex; flex-direction: column; box-shadow: -18px 0 50px rgba(0,0,0,0.6); }
162
+ #howto.open { transform: none; }
163
+ #howto h3 { margin: 0; padding: 0.7rem 1rem; font-size: 13px; color: #F2F2F2; border-bottom: 1px solid var(--line);
164
+ display: flex; align-items: center; gap: 0.5rem; font-weight: 600; flex: 0 0 auto; }
165
+ #howto h3 .x { margin-left: auto; cursor: pointer; color: var(--dim); border: 0; background: transparent; font: inherit; }
166
+ #howto h3 .x:hover { color: #F2F2F2; }
167
+ #howto .doc { overflow: auto; padding: 1.6rem 1.9rem 2.6rem; }
168
+ #howto .col { max-width: 62ch; margin: 0 auto;
169
+ font: 15px/1.62 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; color: #c8ccd2; }
170
+ #howto .hook { font-size: 1.32rem; line-height: 1.4; font-weight: 600; color: #F4F5F6; margin: 0 0 0.4rem; letter-spacing: -0.01em; }
171
+ #howto .lede { color: #aeb4bc; margin: 0 0 1.4rem; }
172
+ #howto h4 { color: #7defc9; font-size: 0.8rem; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase;
173
+ margin: 1.9rem 0 0.55rem; }
174
+ #howto p { margin: 0 0 0.9rem; }
175
+ #howto b, #howto strong { color: #eef0f2; font-weight: 600; }
176
+ #howto .point { margin: 0 0 0.95rem; }
177
+ #howto .point b { display: block; color: #eef0f2; margin-bottom: 0.1rem; }
178
+ #howto ul { margin: 0.3rem 0 0.9rem; padding: 0; list-style: none; }
179
+ #howto li { position: relative; padding-left: 1.1rem; margin: 0 0 0.6rem; }
180
+ #howto li::before { content: "β€Ί"; position: absolute; left: 0; color: #7defc9; font-weight: 700; }
181
+ #howto li b { color: #eef0f2; }
182
+ #howto .chip { font: 600 12.5px ui-monospace, "Cascadia Mono", Consolas, monospace; color: #0C0C0C;
183
+ background: #7defc9; border: 0; border-radius: 5px; padding: 0.06rem 0.42rem; cursor: pointer;
184
+ vertical-align: baseline; transition: filter 0.12s; }
185
+ #howto .chip:hover { filter: brightness(1.08); }
186
+ #howto .ui { font: 600 12.5px ui-monospace, "Cascadia Mono", Consolas, monospace; color: #F9F1A5; }
187
+ #howto .tryrow { display: flex; flex-wrap: wrap; gap: 0.45rem; margin: 0.5rem 0 0.8rem; }
188
+ #howto .note { color: var(--dim); font-size: 13px; margin-top: 0.4rem; }
189
+ #howto hr { border: 0; border-top: 1px solid var(--line); margin: 1.8rem 0; }
190
+
191
+ #toast { position: fixed; bottom: 1rem; right: 1rem; background: #1E1E1E; border: 1px solid #333333;
192
+ color: var(--fg); border-radius: var(--holo-radius-sm, 8px); padding: 0.5rem 0.85rem; font-size: 12.5px; opacity: 0;
193
+ transition: opacity 0.2s; z-index: 9; box-shadow: 0 8px 28px rgba(0,0,0,0.5); }
194
+ #toast.show { opacity: 1; }
195
+
196
+ /* find-bar (xterm search addon) */
197
+ #find { position: absolute; top: 0.5rem; right: 0.6rem; z-index: 6; display: flex; align-items: center; gap: 0.25rem;
198
+ background: #1E1E1E; border: 1px solid #333333; border-radius: 7px; padding: 0.26rem 0.4rem; box-shadow: 0 8px 24px rgba(0,0,0,0.5); }
199
+ #find[hidden] { display: none; }
200
+ #find input { background: #0C0C0C; color: var(--fg); border: 1px solid #333333; border-radius: 5px;
201
+ padding: 0.22rem 0.45rem; font: inherit; width: 13rem; outline: none; }
202
+ #find input:focus { border-color: var(--acc); box-shadow: 0 0 0 3px rgba(59,120,255,0.2); }
203
+ #find .fc { color: var(--faint); font-size: 12px; min-width: 2.6rem; text-align: right; font-variant-numeric: tabular-nums; }
204
+ #find button { color: var(--dim); background: transparent; border: 0; cursor: pointer; padding: 0.12rem 0.4rem; border-radius: 5px; }
205
+ #find button:hover { color: #F2F2F2; background: var(--hover); }
206
+
207
+ /* Entry splash is now the shared <holo-splash> custom element (holo-splash.js) β€”
208
+ one source of truth for every Holo app's "streaming" boot screen. No styles here. */
209
+ </style>
210
+ <link rel="icon" href="./icon.svg">
211
+ <script>try{if("serviceWorker" in navigator)navigator.serviceWorker.register(new URL("../../root-sw.js",location.href),{type:"module"}).catch(function(){navigator.serviceWorker.register(new URL("../../root-sw-classic.js",location.href)).catch(function(){})})}catch(e){}</script>
212
+ </head>
213
+ <body>
214
+ <div id="bar">
215
+ <span class="dot" id="dot">●</span>
216
+ <span class="tag" id="state">starting</span>
217
+ <span class="sp"></span>
218
+ <button id="howtoBtn" title="what this is and why it matters β€” in plain language">β“˜ how it works</button>
219
+ <button id="offlineBtn" title="cache the whole machine so it boots with the server stopped">⬇ make offline</button>
220
+ <button id="airBtn" title="block ALL network at the service worker, then cold-boot from cache only β€” proves it's serverless">✈ offline boot</button>
221
+ <button id="verifyBtn" title="how to verify this is a real kernel">βœ“ verify it yourself</button>
222
+ <button id="tamperBtn" title="flip one byte of the rootfs and watch the κ-gate refuse to boot">⚠ tamper test</button>
223
+ <button id="saveBtn" title="snapshot this machine to local storage now (it also auto-saves)">πŸ’Ύ save</button>
224
+ <button id="newBtn" title="discard the saved machine, re-verify κ, and cold-boot a fresh one">✦ new</button>
225
+ <button id="rebootBtn" title="restart β€” resumes your saved machine">⟳ restart</button>
226
+ <button id="teeBtn" hidden title="seal this machine to your biometric (Windows Hello / Touch ID) β€” only you, on this device, can resume it">πŸ” make sovereign</button>
227
+ </div>
228
+
229
+ <div id="kappa">
230
+ <span class="lbl">ΞΊ-anchored:</span>
231
+ <span class="pin" id="pin-kernel"><span class="v">…</span><span>kernel</span> <span class="k">β€”</span></span>
232
+ <span class="pin" id="pin-rootfs"><span class="v">…</span><span>rootfs</span> <span class="k">β€”</span></span>
233
+ <span class="pin" id="pin-snapshot" hidden><span class="v">…</span><span>snapshot</span> <span class="k">β€”</span></span>
234
+ <span class="sp" style="margin-left:auto"></span>
235
+ <span class="pin" id="saved" title="local persistence β€” your machine is saved on this device">β—‹ not saved</span>
236
+ </div>
237
+
238
+ <div id="stage">
239
+ <div id="term"></div>
240
+ <div id="find" hidden>
241
+ <input id="findq" type="text" placeholder="find" spellcheck="false" autocomplete="off" aria-label="find in terminal" />
242
+ <span id="findcount" class="fc"></span>
243
+ <button id="findprev" title="previous (Shift+Enter)">↑</button>
244
+ <button id="findnext" title="next (Enter)">↓</button>
245
+ <button id="findclose" title="close (Esc)">βœ•</button>
246
+ </div>
247
+
248
+ <div id="overlay">
249
+ <!-- No welcome prose: the <holo-splash> entry screen is the only intro. This panel is
250
+ now just a quiet boot indicator (spinner + status) and the ΞΊ-gate error surface. -->
251
+ <div id="panel">
252
+ <div class="status"><span class="spin" data-holo-spin="cascade"></span><span id="pstatus">loading…</span></div>
253
+ <div class="hint" id="phint"></div>
254
+ </div>
255
+ </div>
256
+
257
+ <div id="scrim"></div>
258
+ <aside id="verify">
259
+ <h3>Verify it yourself <button class="x" id="verifyClose" title="close">βœ•</button></h3>
260
+ <div class="intro">Don't trust the badge β€” prove it. Each line runs in the live shell. First, that this is a real Linux kernel; then the ΞΊ-substrate that lets you build, run and share serverless apps inside sealed, content-verified VMs.</div>
261
+ <div class="list" id="verifyList"></div>
262
+ <div class="foot"><button id="runAll">β–· run all checks</button></div>
263
+ </aside>
264
+
265
+ <aside id="howto">
266
+ <h3>How it works <button class="x" id="howtoClose" title="close">βœ•</button></h3>
267
+ <div class="doc">
268
+ <div class="col">
269
+ <p class="hook">You're running a real computer that no server sent you.</p>
270
+ <p class="lede">This page booted a complete Linux system β€” the same kind that runs much of the internet β€” entirely on your device, inside this browser tab. Nothing runs on a server. No company is in the loop. And before a single instruction ran, your browser checked every byte against a fingerprint, so you don't have to trust anyone.</p>
271
+
272
+ <h4>The problem with normal software</h4>
273
+ <p>Almost everything you run is something you're asked to <b>trust</b>. A server sends it. A company can change it tomorrow. It can break, get pulled, or quietly become something else β€” and you can't inspect it. Usually it won't even work without a connection.</p>
274
+
275
+ <h4>What's different here</h4>
276
+ <p class="point"><b>It runs on your device, not a server.</b> The processor is built in software, running inside this web page. Turn off your internet and keep typing β€” it keeps working. <button class="chip" data-cmd="serverless">serverless</button></p>
277
+ <p class="point"><b>It's checked before it runs.</b> Every part β€” the Linux kernel, the whole filesystem β€” has a <b>fingerprint</b>: a short code derived from its exact bytes. Change one byte and the fingerprint changes completely. Your browser re-computes it and refuses to start if anything is off. <button class="chip" data-cmd="fingerprint">fingerprint</button></p>
278
+ <p class="point"><b>It can't be tampered with unnoticed.</b> Because the identity <em>is</em> the bytes, an altered or corrupted system simply won't boot. <button class="chip" data-cmd="tamper">tamper</button> <span class="note">or hit <span class="ui">⚠ tamper test</span> and watch one flipped byte stop it cold.</span></p>
279
+ <p class="point"><b>Your work stays β€” with no cloud.</b> Files you create live on a real disk inside your browser; they survive a reboot, with no server storing them. <button class="chip" data-cmd="persist">persist</button> <span class="note">then press <span class="ui">⟳ reboot</span> and run it again.</span></p>
280
+ <p class="point"><b>Anyone can reproduce it, exactly.</b> The same fingerprint rebuilds the identical machine for anyone, anywhere, forever β€” no server needed to hand it out. <button class="chip" data-cmd="reproduce">reproduce</button></p>
281
+
282
+ <h4>Why it matters</h4>
283
+ <p>Software you can <b>verify yourself</b> β€” that needs no server, no company, and no connection. Integrity by math, not by trust. You don't take anyone's word for the claims above: each one is a command you can run in the terminal right now, and check the output yourself.</p>
284
+
285
+ <h4>What this unlocks</h4>
286
+ <ul>
287
+ <li><b>Reproducible research</b> β€” share a fingerprint; anyone re-runs the exact same environment and verifies the result themselves.</li>
288
+ <li><b>Verifiable software</b> β€” know precisely which bytes ran; a tampered build can't even start.</li>
289
+ <li><b>Install-free sandboxes</b> β€” a real Linux to teach, learn, or try; offline, nothing to set up.</li>
290
+ <li><b>Air-gapped &amp; high-assurance</b> β€” runs with no network and proves its own integrity; nothing phones home.</li>
291
+ <li><b>Software that lasts</b> β€” defined by its content, not a live server, so it still runs in twenty years.</li>
292
+ </ul>
293
+
294
+ <hr />
295
+ <h4>See for yourself</h4>
296
+ <p>Click any to run it in the terminal β€” no server, no third party, verified in real time:</p>
297
+ <div class="tryrow">
298
+ <button class="chip" data-cmd="verify">verify</button>
299
+ <button class="chip" data-cmd="serverless">serverless</button>
300
+ <button class="chip" data-cmd="tamper">tamper</button>
301
+ <button class="chip" data-cmd="fingerprint">fingerprint</button>
302
+ <button class="chip" data-cmd="persist">persist</button>
303
+ <button class="chip" data-cmd="reproduce">reproduce</button>
304
+ </div>
305
+ </div>
306
+ </div>
307
+ </aside>
308
+
309
+ <div id="toast"></div>
310
+ </div>
311
+
312
+ <!-- Entry splash: the shared Hologram "streaming" boot screen. ONE source of truth for
313
+ every Holo app β€” only the app="…" name differs. Self-contained, offline, Shadow DOM. -->
314
+ <script src="./holo-splash.js"></script>
315
+ <holo-splash app="Holo Linux" manual></holo-splash>
316
+
317
+ <!-- xterm.js 5.5.0 (UMD) + the addons that make it a complete browser terminal: GPU
318
+ render (WebGL), clickable links, search, Unicode-11 widths, OSC-52 clipboard.
319
+ Same components VS Code's terminal ships. Vendored locally so the whole app
320
+ lives under one service-worker scope and boots offline. -->
321
+ <script src="./vendor/xterm/xterm.js"></script>
322
+ <script src="./vendor/xterm/addon-fit.js"></script>
323
+ <script src="./vendor/xterm/addon-webgl.js"></script>
324
+ <script src="./vendor/xterm/addon-web-links.js"></script>
325
+ <script src="./vendor/xterm/addon-search.js"></script>
326
+ <script src="./vendor/xterm/addon-unicode11.js"></script>
327
+ <script src="./vendor/xterm/addon-clipboard.js"></script>
328
+ <script type="module">
329
+ import { spinners } from "./unicode-animations.js";
330
+ import { teeUnlock, teeEnroll, teeAvailable, isSovereign } from "./holo-linux-tee.mjs";
331
+ const $ = (id) => document.getElementById(id);
332
+ const params = new URLSearchParams(location.search);
333
+ // The entry splash is the self-contained <holo-splash> element (holo-splash.js): it shows
334
+ // for ~3s and removes itself, fading into the live boot underneath. Nothing to wire here.
335
+
336
+ // Stable machine id. Local persistence keys the saved machine by this id, so YOUR
337
+ // machine reattaches across a full browser restart β€” localStorage survives a restart
338
+ // (sessionStorage would not). Default is one persistent "main" machine; ?sid=<x> selects
339
+ // an explicit/extra machine. (OPFS hands a machine's files to one tab at a time.)
340
+ let SID = params.get("sid");
341
+ if (!SID) { try { SID = localStorage.getItem("holoLinuxMachine"); } catch (_) {} if (!SID) { SID = "main"; try { localStorage.setItem("holoLinuxMachine", SID); } catch (_) {} } }
342
+ SID = (SID || "main").replace(/[^a-z0-9]/gi, "").slice(0, 16) || "main";
343
+ // ?fresh is a one-shot (discard saved machine + cold boot). Strip it from the address bar
344
+ // so a later manual reload doesn't wipe the machine again; we still hand it to the worker once.
345
+ let freshNext = params.has("fresh");
346
+ if (freshNext) { try { const u = new URL(location.href); u.searchParams.delete("fresh"); history.replaceState(null, "", u); } catch (_) {} }
347
+
348
+ // ── the real terminal (xterm.js) ─────────────────────────────────────
349
+ const term = new Terminal({
350
+ allowProposedApi: true,
351
+ fontFamily: '"Cascadia Mono", "Cascadia Code", Consolas, "JetBrains Mono", ui-monospace, Menlo, monospace',
352
+ fontSize: 14, fontWeight: 400, fontWeightBold: 600, lineHeight: 1.1, letterSpacing: 0,
353
+ cursorBlink: true, cursorStyle: "block", scrollback: 20000, convertEol: false,
354
+ drawBoldTextInBrightColors: true, minimumContrastRatio: 1,
355
+ // Windows-Terminal "Campbell" ANSI β€” the canonical real-CLI dark palette.
356
+ theme: {
357
+ background: "#0C0C0C", foreground: "#CCCCCC", cursor: "#CCCCCC", cursorAccent: "#0C0C0C", selectionBackground: "rgba(255,255,255,0.25)",
358
+ black: "#0C0C0C", red: "#C50F1F", green: "#13A10E", yellow: "#C19C00", blue: "#0037DA",
359
+ magenta: "#881798", cyan: "#3A96DD", white: "#CCCCCC",
360
+ brightBlack: "#767676", brightRed: "#E74856", brightGreen: "#16C60C", brightYellow: "#F9F1A5",
361
+ brightBlue: "#3B78FF", brightMagenta: "#B4009E", brightCyan: "#61D6D6", brightWhite: "#F2F2F2",
362
+ },
363
+ });
364
+ const fit = new FitAddon.FitAddon();
365
+ term.loadAddon(fit);
366
+ term.loadAddon(new WebLinksAddon.WebLinksAddon());
367
+ const uni = new Unicode11Addon.Unicode11Addon();
368
+ term.loadAddon(uni); term.unicode.activeVersion = "11";
369
+ const search = new SearchAddon.SearchAddon();
370
+ term.loadAddon(search);
371
+ try { term.loadAddon(new ClipboardAddon.ClipboardAddon()); } catch (_) {}
372
+ term.open($("term"));
373
+ try { const webgl = new WebglAddon.WebglAddon(); webgl.onContextLoss(() => webgl.dispose()); term.loadAddon(webgl); console.info("[holo-linux] renderer: webgl (GPU)"); } catch (e) { console.warn("[holo-linux] webgl unavailable β€” falling back to canvas/dom:", e && e.message); }
374
+
375
+ let refitT;
376
+ const refit = () => { try { fit.fit(); } catch (_) {} };
377
+ const refitSoon = () => { clearTimeout(refitT); refitT = setTimeout(refit, 60); }; // debounced
378
+ refit(); requestAnimationFrame(refit); // after first layout
379
+ if (document.fonts && document.fonts.ready) document.fonts.ready.then(refit); // after Cascadia Mono loads (correct cell metrics β†’ margin holds)
380
+ new ResizeObserver(refitSoon).observe($("stage")); addEventListener("resize", refitSoon);
381
+ const enc = new TextEncoder();
382
+ const sendRaw = (s) => worker && worker.postMessage({ type: "stdin", data: enc.encode(s) });
383
+
384
+ // Real copy/paste + find: Ctrl+Shift+C/V/F. (Plain Ctrl+C reaches the guest as SIGINT.)
385
+ term.attachCustomKeyEventHandler((e) => {
386
+ if (e.type !== "keydown" || !e.ctrlKey || !e.shiftKey) return true;
387
+ const k = e.key.toLowerCase();
388
+ if (k === "c") { const s = term.getSelection(); if (s) navigator.clipboard?.writeText(s); return false; }
389
+ if (k === "v") { navigator.clipboard?.readText().then(sendRaw); return false; }
390
+ if (k === "f") { toggleFind(true); return false; }
391
+ return true;
392
+ });
393
+ $("term").addEventListener("paste", (e) => { const t = e.clipboardData?.getData("text"); if (t) sendRaw(t); });
394
+
395
+ // ── find-bar ─────────────────────────────────────────────────────────
396
+ const FIND_OPTS = { decorations: { matchBackground: "#5A4711", activeMatchBackground: "#E3B341",
397
+ matchOverviewRuler: "#E3B341", activeMatchColorOverviewRuler: "#FF8C00" } };
398
+ function doFind(prev) { const q = $("findq").value; if (!q) { search.clearDecorations?.(); $("findcount").textContent = ""; return; }
399
+ if (prev) search.findPrevious(q, FIND_OPTS); else search.findNext(q, FIND_OPTS); }
400
+ function toggleFind(show) { const el = $("find"); const on = show === undefined ? el.hidden : show; el.hidden = !on;
401
+ if (on) { $("findq").focus(); $("findq").select(); doFind(false); } else { search.clearDecorations?.(); $("findcount").textContent = ""; term.focus(); } }
402
+ search.onDidChangeResults?.((r) => { $("findcount").textContent = !r || !r.resultCount ? "0/0" : `${r.resultIndex + 1}/${r.resultCount}`; });
403
+ $("findq").addEventListener("input", () => doFind(false));
404
+ $("findq").addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); doFind(e.shiftKey); } else if (e.key === "Escape") { e.preventDefault(); toggleFind(false); } });
405
+ $("findnext").addEventListener("click", () => doFind(false));
406
+ $("findprev").addEventListener("click", () => doFind(true));
407
+ $("findclose").addEventListener("click", () => toggleFind(false));
408
+
409
+ // ── overlay + chrome ─────────────────────────────────────────────────
410
+ const setStatus = (t) => ($("pstatus").textContent = t);
411
+ const hideOverlay = () => $("overlay").classList.add("hidden");
412
+ const setState = (t, cls) => { $("state").textContent = t; $("dot").className = "dot" + (cls ? " " + cls : ""); };
413
+ const fail = (text, hint) => { $("overlay").classList.remove("hidden"); $("panel").classList.add("err"); setStatus(text); $("phint").innerHTML = hint || ""; setState("error", "off"); };
414
+ let toastT; const toast = (m) => { const el = $("toast"); el.textContent = m; el.classList.add("show"); clearTimeout(toastT); toastT = setTimeout(() => el.classList.remove("show"), 1800); };
415
+
416
+ // ── ΞΊ rail ───────────────────────────────────────────────────────────
417
+ const shortK = (did) => { const h = String(did).split(":").pop(); return "did:holo:sha256:" + h.slice(0, 10) + "…" + h.slice(-6); };
418
+ function renderPin(m) {
419
+ const el = $("pin-" + m.which); if (!el) return;
420
+ el.hidden = false;
421
+ el.className = "pin " + (m.ok ? "ok" : "bad");
422
+ el.title = `${m.name}\n${m.actual}` + (m.ok ? "" : `\nEXPECTED ${m.expected}`);
423
+ const canonical = m.canonical ? `<span class="badge" title="this ΞΊ equals the canonical substrate pin at ${m.canonical}">= ${m.canonical}</span>` : "";
424
+ el.innerHTML = `<span class="v">${m.ok ? "βœ“" : "βœ—"}</span><span>${m.which}</span> <span class="k">${shortK(m.actual)}</span>${canonical}`;
425
+ }
426
+
427
+ // ── boot animation ───────────────────────────────────────────────────
428
+ const ANIM = spinners[params.get("anim")] || spinners.dna;
429
+ let animTimer = null, animFrame = 0, animLabel = "";
430
+ function startAnim(label) { stopAnim(); animLabel = label || ""; term.write("\x1b[?25l");
431
+ const draw = () => { const f = ANIM.frames[(animFrame = (animFrame + 1) % ANIM.frames.length)]; term.write("\r\x1b[2K\x1b[38;5;111m" + f + "\x1b[0m \x1b[90m" + animLabel + "\x1b[0m"); };
432
+ draw(); animTimer = setInterval(draw, ANIM.interval || 90); }
433
+ const setAnimLabel = (t) => { animLabel = t; };
434
+ function stopAnim(clearLine) { if (animTimer) { clearInterval(animTimer); animTimer = null; } if (clearLine) term.write("\r\x1b[2K"); term.write("\x1b[?25h"); }
435
+
436
+ // ── HOLO LINUX wordmark β€” figlet "ANSI Shadow", rainbow (the patorjk TAAG render) ──
437
+ // Fixed-width 6-row glyphs concatenated per row, so the letters stay perfectly aligned;
438
+ // every cell is then coloured along a smooth rainbow (rainbow3-style).
439
+ const SHADOW = {
440
+ H: ["β–ˆβ–ˆβ•— β–ˆβ–ˆβ•—", "β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘", "β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘", "β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘", "β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘", "β•šβ•β• β•šβ•β•"],
441
+ O: [" β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— ", "β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—", "β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘", "β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘", "β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•", " β•šβ•β•β•β•β•β• "],
442
+ L: ["β–ˆβ–ˆβ•— ", "β–ˆβ–ˆβ•‘ ", "β–ˆβ–ˆβ•‘ ", "β–ˆβ–ˆβ•‘ ", "β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—", "β•šβ•β•β•β•β•β•β•"],
443
+ I: ["β–ˆβ–ˆβ•—", "β–ˆβ–ˆβ•‘", "β–ˆβ–ˆβ•‘", "β–ˆβ–ˆβ•‘", "β–ˆβ–ˆβ•‘", "β•šβ•β•"],
444
+ N: ["β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•—", "β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘", "β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘", "β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘", "β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘", "β•šβ•β• β•šβ•β•β•β•"],
445
+ U: ["β–ˆβ–ˆβ•— β–ˆβ–ˆβ•—", "β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘", "β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘", "β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘", "β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•", " β•šβ•β•β•β•β•β• "],
446
+ X: ["β–ˆβ–ˆβ•— β–ˆβ–ˆβ•—", "β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•", " β•šβ–ˆβ–ˆβ–ˆβ•”β• ", " β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— ", "β–ˆβ–ˆβ•”β• β–ˆβ–ˆβ•—", "β•šβ•β• β•šβ•β•"],
447
+ " ": [" ", " ", " ", " ", " ", " "],
448
+ };
449
+ const wordmark = (text) => [0, 1, 2, 3, 4, 5].map((r) =>
450
+ text.toUpperCase().split("").map((c) => (SHADOW[c] || SHADOW[" "])[r]).join(""));
451
+ const WORDMARK = wordmark("HOLO LINUX");
452
+ const hsl2rgb = (h, s, l) => {
453
+ const c = (1 - Math.abs(2 * l - 1)) * s, x = c * (1 - Math.abs(((h / 60) % 2) - 1)), m = l - c / 2;
454
+ let r = 0, g = 0, b = 0;
455
+ if (h < 60) { r = c; g = x; } else if (h < 120) { r = x; g = c; } else if (h < 180) { g = c; b = x; }
456
+ else if (h < 240) { g = x; b = c; } else if (h < 300) { r = x; b = c; } else { r = c; b = x; }
457
+ return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
458
+ };
459
+ function writeWordmark() {
460
+ term.write("\r\n");
461
+ const period = 26; // columns per full rainbow turn
462
+ for (let r = 0; r < WORDMARK.length; r++) {
463
+ const line = WORDMARK[r];
464
+ let out = " ";
465
+ for (let col = 0; col < line.length; col++) {
466
+ const ch = line[col];
467
+ if (ch === " ") { out += " "; continue; }
468
+ const hue = (((col + r * 2) * 360) / period) % 360; // smooth, slightly diagonal
469
+ const rgb = hsl2rgb(hue, 1, 0.6);
470
+ out += "\x1b[1;38;2;" + rgb[0] + ";" + rgb[1] + ";" + rgb[2] + "m" + ch;
471
+ }
472
+ term.write(out + "\x1b[0m\r\n");
473
+ }
474
+ term.write("\r\n \x1b[38;5;245ma real riscv64 Debian β€” streamed, sealed and ΞΊ-verified\x1b[0m\r\n\r\n");
475
+ }
476
+
477
+ // ── braille "alive" heartbeat β€” a living pulse in the status pill while running ──
478
+ const BRAILLE = ["β ‹", "β ™", "β Ή", "β Έ", "β Ό", "β ΄", "β ¦", "β §", "β ‡", "⠏"];
479
+ let hbFrame = 0, hbTimer = null;
480
+ function startHeartbeat() { stopHeartbeat(); hbTimer = setInterval(() => { $("dot").textContent = BRAILLE[hbFrame = (hbFrame + 1) % BRAILLE.length]; }, 110); }
481
+ function stopHeartbeat() { if (hbTimer) { clearInterval(hbTimer); hbTimer = null; } $("dot").textContent = "●"; }
482
+
483
+ // ── "verify it yourself" β€” real commands into the live shell ──────────
484
+ // Every command here is REAL and EXISTS in this rootfs (probed live: no cc, no
485
+ // ps, no editor β€” so none are used). Each reads kernel-synthesised state a
486
+ // scripted shell cannot reproduce. `holo-verify` is the in-guest companion.
487
+ const CHECKS = [
488
+ // ── prove it's a real Linux kernel (stock commands, nothing holo-specific) ──
489
+ { group: "Real Linux β€” prove the kernel", cmd: "uname -a", why: "Release, SMP build and arch riscv64 β€” straight from the running kernel." },
490
+ { cmd: "grep -E 'isa|mmu|hart' /proc/cpuinfo", why: "/proc is synthesised by the kernel: real RISC-V harts, ISA extensions, sv57 MMU." },
491
+ { cmd: "head -4 /etc/os-release", why: "A genuine Debian 13 (trixie) userland β€” a full distro, not a shim." },
492
+ { cmd: "dmesg | tail -8", why: "The kernel's own boot ring buffer β€” a scripted shell has no dmesg to show." },
493
+ { cmd: "ls -d /proc/[0-9]* | sed 's#/proc/##' | tr '\\n' ' '; echo; cat /proc/1/comm", why: "Real PIDs from /proc (no ps needed); PID 1 is init. A mock has no process table." },
494
+ { cmd: "cat /proc/sys/kernel/random/uuid; cat /proc/sys/kernel/random/uuid", why: "Two reads of the kernel CSPRNG β†’ two different UUIDs, not a constant." },
495
+ // ── the ΞΊ-substrate story: content-addressed, isolated, yours to build on ──
496
+ { group: "ΞΊ-substrate β€” content-addressed Β· isolated Β· serverless", cmd: "mount | grep -E 'ext4|proc|sysfs|devtmpfs'", why: "A real VFS: an ext4 root on the ΞΊ-verified virtio disk, plus proc/sysfs/devtmpfs." },
497
+ { cmd: "sha256sum /etc/os-release /etc/profile.d/00-holo-linux.sh", why: "Every byte of this rootfs is admitted by content address (Law L5). These digests are re-derived at boot β€” flip one byte and the ΞΊ-gate refuses to start." },
498
+ { cmd: "echo 'serverless app state' > /workspace/app.txt && sync && cat /workspace/app.txt && sha256sum /workspace/app.txt", why: "Build + run in place: write to the persistent ΞΊ-disk, sync, hash it back. App state round-trips through a real, content-addressed filesystem." },
499
+ { cmd: "for c in ip ping curl wget ssh nc; do command -v \"$c\" >/dev/null && echo \"$c present\"; done; echo '(no network tools listed above => this VM is sealed and cannot phone home)'", why: "Secure isolation: the machine ships with no network stack. Nothing enters or leaves the sandbox." },
500
+ { cmd: "df -h /", why: "The ΞΊ-disk itself β€” capacity and live usage of the ext4 root you build on." },
501
+ ];
502
+ function buildChecks() {
503
+ const list = $("verifyList"); list.innerHTML = ""; let curGroup = null;
504
+ for (const c of CHECKS) {
505
+ if (c.group && c.group !== curGroup) { curGroup = c.group; const h = document.createElement("div"); h.className = "ghead"; h.textContent = c.group; list.append(h); }
506
+ const div = document.createElement("div"); div.className = "chk";
507
+ const why = document.createElement("div"); why.className = "why"; why.textContent = c.why;
508
+ const row = document.createElement("div"); row.className = "row";
509
+ const code = document.createElement("code"); code.textContent = c.cmd;
510
+ const btn = document.createElement("button"); btn.textContent = "run"; btn.title = "type + run in the live shell";
511
+ btn.onclick = () => { closeVerify(); term.focus(); sendRaw(c.cmd + "\n"); };
512
+ row.append(code, btn); div.append(why, row); list.append(div);
513
+ }
514
+ }
515
+ const closePanels = () => { $("verify").classList.remove("open"); $("howto").classList.remove("open"); $("scrim").classList.remove("open"); };
516
+ const openVerify = () => { closePanels(); $("verify").classList.add("open"); $("scrim").classList.add("open"); };
517
+ const openHowto = () => { closePanels(); $("howto").classList.add("open"); $("scrim").classList.add("open"); };
518
+ const closeVerify = closePanels; // run buttons/runAll close whatever panel is open
519
+ $("verifyBtn").onclick = openVerify; $("verifyClose").onclick = closePanels; $("scrim").onclick = closePanels;
520
+ $("howtoBtn").onclick = openHowto; $("howtoClose").onclick = closePanels;
521
+ // the "How it works" command chips run the real command in the live shell, then close.
522
+ $("howto").addEventListener("click", (e) => {
523
+ const chip = e.target.closest(".chip"); if (!chip) return;
524
+ const cmd = chip.getAttribute("data-cmd"); if (!cmd) return;
525
+ closePanels(); term.focus(); sendRaw(cmd + "\n");
526
+ });
527
+ // Esc closes an open panel (only then β€” otherwise the terminal keeps Esc for itself).
528
+ addEventListener("keydown", (e) => { if (e.key === "Escape" && ($("verify").classList.contains("open") || $("howto").classList.contains("open"))) { e.preventDefault(); closePanels(); } });
529
+ $("runAll").onclick = () => { closeVerify(); term.focus(); let i = 0; const next = () => { if (i >= CHECKS.length) return; sendRaw(CHECKS[i++].cmd + "\n"); setTimeout(next, 700); }; next(); };
530
+ buildChecks();
531
+
532
+ // ── drive the entry splash from REAL boot progress ───────────────────
533
+ // The <holo-splash> bar tracks actual milestones (the ΞΊ-disk provisioning is the headline
534
+ // step) and completes the instant the machine starts streaming (booted) β€” so the splash
535
+ // lasts exactly as long as the boot/provision takes, then fades into the live stream.
536
+ const splash = document.querySelector("holo-splash");
537
+ const splashTo = (f) => { try { splash && splash.progress && splash.progress(f); } catch (_) {} };
538
+ const splashDone = () => { try { splash && splash.complete && splash.complete(); } catch (_) {} };
539
+ const splashAway = () => { try { splash && splash.dismiss && splash.dismiss(); } catch (_) {} };
540
+ function statusProgress(t) {
541
+ t = String(t || "");
542
+ if (/loading the machine/i.test(t)) return 0.06;
543
+ if (/fetching/i.test(t)) return 0.16;
544
+ if (/re-deriving.*kernel/i.test(t)) return 0.26;
545
+ if (/re-deriving.*rootfs/i.test(t)) return 0.38;
546
+ if (/verified/i.test(t)) return 0.48;
547
+ if (/assembling/i.test(t)) return 0.56;
548
+ if (/provisioning/i.test(t)) return 0.66; // ← the ΞΊ-disk space being provisioned
549
+ if (/powering on/i.test(t)) return 0.80;
550
+ return 0;
551
+ }
552
+
553
+ // ── the boot worker ──────────────────────────────────────────────────
554
+ let worker = null;
555
+ let tamperNext = false; // one-shot: next boot flips a rootfs byte to prove the ΞΊ-gate
556
+ let airMode = false; // true once "offline boot" has blocked the network at the SW
557
+ let resumedInfo = null; // set when the worker resumed a saved machine (CC-30)
558
+ // Local-persistence "saved" chip in the ΞΊ rail.
559
+ function setSaved(on, title) { const el = $("saved"); if (!el) return; el.className = "pin " + (on ? "ok" : ""); el.innerHTML = on ? '<span class="v">πŸ”’</span><span>sealed</span>' : "β—‹ not saved"; if (title) el.title = title; }
560
+ // TEE biometric unlock β€” once per page session (cached). On a device with a platform authenticator
561
+ // (Touch ID / Windows Hello / native holo:hello) this is one biometric and the seal key becomes
562
+ // enclave-derived (attestRoot "tee"); with none, teeUnlock returns null β†’ soft, device-local key.
563
+ let teeSecret = null, teeTried = false;
564
+ async function ensureTee() { if (!teeTried) { teeTried = true; try { teeSecret = await teeUnlock(SID); } catch (_) { teeSecret = null; } } return teeSecret; }
565
+ async function startWorker() {
566
+ $("overlay").classList.remove("hidden"); $("panel").classList.remove("err"); $("phint").textContent = "";
567
+ setState("booting", ""); stopHeartbeat(); resumedInfo = null; { const mEl = $("mips"); if (mEl) mEl.textContent = ""; }
568
+ for (const w of ["kernel", "rootfs", "snapshot"]) { const el = $("pin-" + w); if (!el) continue; el.className = "pin"; el.hidden = (w === "snapshot"); el.innerHTML = `<span class="v">…</span><span>${w}</span> <span class="k">β€”</span>`; }
569
+ term.reset();
570
+ writeWordmark();
571
+ startAnim("ΞΊ-verifying the kernel + rootfs, then powering on…");
572
+ const secret = await ensureTee(); // one biometric per session (instant null on devices without a platform authenticator)
573
+ const q = new URLSearchParams();
574
+ for (const k of ["disk", "v", "autosave"]) { const v = params.get(k); if (v) q.set(k, v); }
575
+ q.set("sid", SID);
576
+ if (secret) q.set("tee", "1"); // the worker awaits the PRF secret before deriving the seal key
577
+ if (tamperNext) { q.set("tamper", "1"); tamperNext = false; }
578
+ if (freshNext) { q.set("fresh", "1"); freshNext = false; } // one-shot: discard saved machine + cold boot
579
+ worker = new Worker("./holo-linux-worker.js?" + q, { type: "module" });
580
+ worker.onmessage = onMessage;
581
+ worker.onerror = (e) => { splashAway(); stopAnim(true); fail("worker failed to start", String(e.message || e)); };
582
+ if (secret) worker.postMessage({ type: "unlock", secret: secret.slice(0) }); // copy (keep cached secret for later restarts)
583
+ }
584
+ function onMessage(e) {
585
+ const m = e.data;
586
+ switch (m.type) {
587
+ case "status": setAnimLabel(m.text); setStatus(m.text); { const p = statusProgress(m.text); if (p) splashTo(p); } break;
588
+ case "kappa": renderPin(m); splashTo(m.which === "kernel" ? 0.30 : 0.42); if (!m.ok) { toast("ΞΊ mismatch on " + m.which + " β€” boot refused"); } break;
589
+ case "resumed": resumedInfo = m; break; // arrives just before "booted" β€” adjusts the running label + saved chip
590
+ case "booted": splashDone(); stopAnim(true); term.write("\r\n"); hideOverlay();
591
+ setState(resumedInfo ? (airMode ? "resumed Β· offline βœ“" : "resumed βœ“") : (airMode ? "running Β· offline βœ“" : "running"), "on");
592
+ startHeartbeat(); term.focus(); refit(); requestAnimationFrame(refit); setTimeout(refit, 250);
593
+ if (resumedInfo) { setSaved(true, "resumed your sealed machine (" + fmtMB(resumedInfo.bytes || 0) + ", decrypted + owner-verified on this device)"); toast("resumed your sealed machine β€” decrypted on this device, files + session intact"); }
594
+ if (airMode) { setOffline("● serverless βœ“", "var(--ok)", "This machine cold-booted with ALL network blocked at the service worker β€” zero bytes from any server."); toast("booted with network blocked β€” 100% serverless, running entirely in this tab"); }
595
+ break;
596
+ case "stdout": term.write(new Uint8Array(m.data)); break;
597
+ case "mips": { const mEl = $("mips"); if (mEl) mEl.textContent = m.mips >= 0.05 ? m.mips.toFixed(1) + " MIPS" : ""; } break;
598
+ case "suspended": setSaved(true, "sealed on this device" + (m.attestRoot ? " (" + m.attestRoot + ")" : "") + " Β· " + fmtMB(m.gzBytes || 0) + " Β· " + shortK(m.kappa));
599
+ if (m.reason === "sovereign") toast("βœ“ sovereign β€” sealed to your biometric (" + (m.attestRoot || "") + "); only you can resume it on this device");
600
+ else if (m.reason === "manual") toast("machine sealed β€” encrypted + owner-signed on this device; it resumes next time");
601
+ reflectTee(); break;
602
+ case "persist-error": toast(m.text || "couldn't save the machine"); break;
603
+ case "halt": splashAway(); stopAnim(true); stopHeartbeat(); setState("powered off", "off"); term.write("\r\n\x1b[90m " + (m.reason || "halted") + " · press ⟳ restart \x1b[0m\r\n"); break;
604
+ case "error": splashAway(); stopAnim(true); stopHeartbeat(); fail(m.text, m.hint ? String(m.hint).replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c])) : ""); break;
605
+ }
606
+ }
607
+ term.onData((d) => sendRaw(d));
608
+ $("rebootBtn").addEventListener("click", () => { tamperNext = false; if (airMode) { airMode = false; swPost({ type: "holo-netblock", on: false }); refreshOffline(); } if (worker) worker.terminate(); startWorker(); });
609
+ // ── local persistence (CC-30): save now Β· new machine Β· auto-save on tab hide/close ──
610
+ const saveNow = (reason) => { if (worker) worker.postMessage({ type: "suspend", reason }); };
611
+ $("saveBtn").addEventListener("click", () => { saveNow("manual"); toast("saving this machine…"); });
612
+
613
+ // ── make sovereign (M2): seal the machine to your biometric (Windows Hello / Touch ID) ──
614
+ // Default tier is "soft" (device-local key, NO prompt on boot). This opt-in enrolls a platform
615
+ // credential, then re-seals the CURRENT live machine under a key HKDF'd from the biometric PRF secret
616
+ // (attestRoot "tee"). We re-seal LIVE state β€” never re-open the soft snapshot with the tee key β€” so the
617
+ // migration can't fail an AEAD check. After this, every resume needs the biometric; a copied blob can't open.
618
+ function reflectTee() {
619
+ const btn = $("teeBtn"); if (!btn) return;
620
+ if (isSovereign(SID)) { btn.hidden = false; btn.disabled = true; btn.textContent = "πŸ”’ sovereign"; btn.title = "this machine is sealed to your biometric on this device β€” resuming it requires Windows Hello / Touch ID"; }
621
+ else { btn.hidden = false; btn.disabled = false; btn.textContent = "πŸ” make sovereign"; }
622
+ }
623
+ teeAvailable().then((ok) => { const b = $("teeBtn"); if (!b) return; if (!ok) { b.hidden = true; } else reflectTee(); });
624
+ $("teeBtn").addEventListener("click", async () => {
625
+ const btn = $("teeBtn"); btn.disabled = true; btn.textContent = "… confirm with your device";
626
+ toast("confirm with your device β€” Windows Hello may ask twice the first time");
627
+ let secret = null;
628
+ try { secret = await teeEnroll(SID); } catch (_) { secret = null; }
629
+ if (!secret) { toast("no biometric enrolled β€” staying on the soft device key"); reflectTee(); return; }
630
+ teeSecret = secret; teeTried = true; // restarts reuse it (one prompt to resume)
631
+ if (worker) worker.postMessage({ type: "unlock", secret: secret.slice(0) }); // re-key the LIVE worker
632
+ saveNow("sovereign"); // re-seal current state under the TEE key
633
+ toast("sealing to your biometric…");
634
+ reflectTee();
635
+ });
636
+ $("newBtn").addEventListener("click", () => {
637
+ freshNext = true; tamperNext = false;
638
+ if (airMode) { airMode = false; swPost({ type: "holo-netblock", on: false }); refreshOffline(); }
639
+ if (worker) worker.terminate(); startWorker(); toast("starting a fresh machine β€” your previous one is discarded");
640
+ });
641
+ // Persist on page-lifecycle signals: tab hidden fires while the page is still alive (reliable);
642
+ // pagehide is best-effort on close. With the worker's periodic autosave, your machine survives a
643
+ // close/restart even if you never press πŸ’Ύ.
644
+ document.addEventListener("visibilitychange", () => { if (document.visibilityState === "hidden") saveNow("hidden"); });
645
+ addEventListener("pagehide", () => saveNow("pagehide"));
646
+ $("tamperBtn").addEventListener("click", () => {
647
+ toast("tamper test: flipping 1 rootfs byte β€” the ΞΊ-gate should refuse");
648
+ tamperNext = true; if (worker) worker.terminate(); startWorker();
649
+ });
650
+ $("airBtn").addEventListener("click", () => {
651
+ if (!navigator.serviceWorker || !navigator.serviceWorker.controller) return toast("service worker still registering β€” try again in a second");
652
+ airMode = true; swPost({ type: "holo-netblock", on: true });
653
+ setOffline("✈ network blocked", "var(--acc)", "The service worker is refusing ALL network. Cold-booting from cache only.");
654
+ toast("network blocked at the service worker β€” cold-booting from cache only");
655
+ setTimeout(() => { if (worker) worker.terminate(); startWorker(); }, 200);
656
+ });
657
+
658
+ // ── offline / serverless caching (driven by the service worker) ──────
659
+ const offlineEl = $("offline");
660
+ const fmtMB = (b) => (b / (1024 * 1024)).toFixed(0) + " MB";
661
+ function setOffline(text, color, title) { if (!offlineEl) return; offlineEl.textContent = text; offlineEl.style.color = color || "var(--dim)"; if (title != null) offlineEl.title = title; }
662
+ function swPost(msg) { const c = navigator.serviceWorker && navigator.serviceWorker.controller; if (c) c.postMessage(msg); }
663
+ const refreshOffline = () => swPost({ type: "holo-cache-status" });
664
+ if ("serviceWorker" in navigator) {
665
+ navigator.serviceWorker.addEventListener("message", (e) => {
666
+ const m = e.data || {};
667
+ if (m.type === "holo-cache-status") {
668
+ if (m.ready) setOffline("● offline ready", "var(--ok)", `All ${m.total} assets cached (${fmtMB(m.bytes)}) β€” stop the server and reboot, it still runs.`);
669
+ else setOffline(`β—‹ ${m.cached}/${m.total} cached`, "var(--dim)", "Click β€œmake offline” to cache the whole machine for serverless boot.");
670
+ } else if (m.type === "holo-warm-progress") {
671
+ setOffline(`⬇ caching ${m.done}/${m.total}`, "var(--acc)", `${fmtMB(m.bytes)} cached`);
672
+ } else if (m.type === "holo-warm-done") {
673
+ setOffline("● offline ready", "var(--ok)", `Cached ${m.done}/${m.total} (${fmtMB(m.bytes)}). Stop the server and press ⟳ reboot.`);
674
+ toast(`offline ready β€” ${fmtMB(m.bytes)} cached. You can stop the server now.`);
675
+ }
676
+ });
677
+ navigator.serviceWorker.ready.then(refreshOffline);
678
+ setInterval(refreshOffline, 4000);
679
+ } else { setOffline("offline n/a", "var(--dim)", "this browser has no service worker"); }
680
+ $("offlineBtn").addEventListener("click", () => {
681
+ if (!navigator.serviceWorker || !navigator.serviceWorker.controller) return toast("service worker still registering β€” try again in a second");
682
+ setOffline("⬇ caching…", "var(--acc)"); swPost({ type: "holo-warm" });
683
+ });
684
+
685
+ // A handle for automation / the witness.
686
+ window.holoLinux = {
687
+ term, fit, search, send: sendRaw, reboot: () => $("rebootBtn").click(), offline: refreshOffline,
688
+ sid: SID,
689
+ save: (reason) => saveNow(reason || "manual"), // snapshot now
690
+ restart: () => { if (worker) worker.terminate(); startWorker(); }, // resume the saved machine
691
+ fresh: () => { freshNext = true; if (worker) worker.terminate(); startWorker(); }, // discard + cold boot
692
+ };
693
+ startWorker();
694
+ </script>
695
+ </body>
696
+ </html>
b/86444b599981098edbc377ed17cf81236efd56321edb86119dcf516f80fc2632 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a7bb1f02a5ac96371ecb402645d25e1cc7cda18c5280f0828f0e31c4fb16162e
3
+ size 6282087
b/95866a33a0ac86be3a1c8b454dd23b1c6279133ff195dd8d73c3ca98cea1a31c ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.39.0.
3
+ * Original file: /npm/unicode-animations@1.0.3/dist/index.js
4
+ *
5
+ * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
6
+ */
7
+ var t=[[1,8],[2,16],[4,32],[64,128]];function r(r){const e=r.length,n=r[0]?r[0].length:0,o=Math.ceil(n/2);let s="";for(let f=0;f<o;f++){let o=10240;for(let s=0;s<4&&s<e;s++)for(let e=0;e<2;e++){const l=2*f+e;l<n&&r[s]&&r[s][l]&&(o|=t[s][e])}s+=String.fromCodePoint(o)}return s}function e(t,r){return t<=0||r<=0?[]:Array.from({length:t},(()=>Array(r).fill(!1)))}var n={braille:{frames:["β ‹","β ™","β Ή","β Έ","β Ό","β ΄","β ¦","β §","β ‡","⠏"],interval:80},braillewave:{frames:["⠁⠂⠄⑀","β ‚β „β‘€β’€","β „β‘€β’€β  ","⑀⒀⠠⠐","β’€β  β β ˆ","⠠⠐⠈⠁","β β ˆβ β ‚","β ˆβ β ‚β „"],interval:100},dna:{frames:["β ‹β ‰β ™β š","β ‰β ™β šβ ’","β ™β šβ ’β ‚","β šβ ’β ‚β ‚","β ’β ‚β ‚β ’","β ‚β ‚β ’β ²","β ‚β ’β ²β ΄","β ’β ²β ΄β €","β ²β ΄β €β „","β ΄β €β „β ‹","β €β „β ‹β ‰","β „β ‹β ‰β ™"],interval:80},scan:{frames:function(){const t=[];for(let n=-1;n<9;n++){const o=e(4,8);for(let t=0;t<4;t++)for(let r=0;r<8;r++)r!==n&&r!==n-1||(o[t][r]=!0);t.push(r(o))}return t}(),interval:70},rain:{frames:function(){const t=[],n=[0,3,1,5,2,7,4,6];for(let o=0;o<12;o++){const s=e(4,8);for(let t=0;t<8;t++){const r=(o+n[t])%6;r<4&&(s[r][t]=!0)}t.push(r(s))}return t}(),interval:100},scanline:{frames:function(){const t=[],n=[0,1,2,3,2,1];for(const o of n){const n=e(4,6);for(let t=0;t<6;t++)n[o][t]=!0,o>0&&(n[o-1][t]=t%2==0);t.push(r(n))}return t}(),interval:120},pulse:{frames:function(){const t=[],n=[.5,1.2,2,3,3.5];for(const o of n){const n=e(4,6);for(let t=0;t<4;t++)for(let r=0;r<6;r++){const e=Math.sqrt((r-2.5)**2+(t-1.5)**2);Math.abs(e-o)<.9&&(n[t][r]=!0)}t.push(r(n))}return t}(),interval:180},snake:{frames:function(){const t=[];for(let r=0;r<4;r++)if(r%2==0)for(let e=0;e<4;e++)t.push([r,e]);else for(let e=3;e>=0;e--)t.push([r,e]);const n=[];for(let o=0;o<t.length;o++){const s=e(4,4);for(let r=0;r<4;r++){const e=(o-r+t.length)%t.length;s[t[e][0]][t[e][1]]=!0}n.push(r(s))}return n}(),interval:80},sparkle:{frames:function(){const t=[[1,0,0,1,0,0,1,0,0,0,1,0,0,1,0,0,0,1,0,0,1,0,0,1,1,0,0,0,0,1,0,0],[0,1,0,0,1,0,0,1,1,0,0,1,0,0,0,1,0,0,0,1,0,1,0,0,0,0,1,0,1,0,1,0],[0,0,1,0,0,1,0,0,0,1,0,0,0,0,1,0,1,0,1,0,0,0,0,1,0,1,0,1,0,0,0,1],[1,0,0,0,0,0,1,1,0,0,1,0,1,0,0,0,0,0,0,0,1,0,1,0,1,0,0,1,0,0,1,0],[0,0,0,1,1,0,0,0,0,1,0,0,0,1,0,1,1,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1],[0,1,1,0,0,0,0,1,0,0,0,1,0,0,1,0,0,1,0,0,0,1,0,0,0,0,1,0,1,0,0,0]],n=[];for(const o of t){const t=e(4,8);for(let r=0;r<4;r++)for(let e=0;e<8;e++)t[r][e]=!!o[8*r+e];n.push(r(t))}return n}(),interval:150},cascade:{frames:function(){const t=[];for(let n=-2;n<12;n++){const o=e(4,8);for(let t=0;t<4;t++)for(let r=0;r<8;r++){const e=r+t;e!==n&&e!==n-1||(o[t][r]=!0)}t.push(r(o))}return t}(),interval:60},columns:{frames:function(){const t=[];for(let n=0;n<6;n++)for(let o=3;o>=0;o--){const s=e(4,6);for(let t=0;t<n;t++)for(let r=0;r<4;r++)s[r][t]=!0;for(let t=o;t<4;t++)s[t][n]=!0;t.push(r(s))}const n=e(4,6);for(let t=0;t<4;t++)for(let r=0;r<6;r++)n[t][r]=!0;return t.push(r(n)),t.push(r(e(4,6))),t}(),interval:60},orbit:{frames:function(){const t=[[0,0],[0,1],[1,1],[2,1],[3,1],[3,0],[2,0],[1,0]],n=[];for(let o=0;o<t.length;o++){const s=e(4,2);s[t[o][0]][t[o][1]]=!0;const f=(o-1+t.length)%t.length;s[t[f][0]][t[f][1]]=!0,n.push(r(s))}return n}(),interval:100},breathe:{frames:function(){const t=[[],[[1,0]],[[0,1],[2,0]],[[0,0],[1,1],[3,0]],[[0,0],[1,1],[2,0],[3,1]],[[0,0],[0,1],[1,1],[2,0],[3,1]],[[0,0],[0,1],[1,0],[2,1],[3,0],[3,1]],[[0,0],[0,1],[1,0],[1,1],[2,0],[3,0],[3,1]],[[0,0],[0,1],[1,0],[1,1],[2,0],[2,1],[3,0],[3,1]]],n=[],o=[...t,...t.slice().reverse().slice(1)];for(const t of o){const o=e(4,2);for(const[r,e]of t)o[r][e]=!0;n.push(r(o))}return n}(),interval:100},waverows:{frames:function(){const t=[];for(let n=0;n<16;n++){const o=e(4,8);for(let t=0;t<8;t++){const r=n-.5*t,e=Math.round((Math.sin(.8*r)+1)/2*3);o[e][t]=!0,e>0&&(o[e-1][t]=(n+t)%3==0)}t.push(r(o))}return t}(),interval:90},checkerboard:{frames:function(){const t=[];for(let n=0;n<4;n++){const o=e(4,6);for(let t=0;t<4;t++)for(let r=0;r<6;r++)o[t][r]=n<2?(t+r+n)%2==0:(t+r+n)%3==0;t.push(r(o))}return t}(),interval:250},helix:{frames:function(){const t=[];for(let n=0;n<16;n++){const o=e(4,8);for(let t=0;t<8;t++){const r=(n+t)*(Math.PI/4),e=Math.round((Math.sin(r)+1)/2*3),s=Math.round((Math.sin(r+Math.PI)+1)/2*3);o[e][t]=!0,o[s][t]=!0}t.push(r(o))}return t}(),interval:80},fillsweep:{frames:function(){const t=[];for(let n=3;n>=0;n--){const o=e(4,4);for(let t=n;t<4;t++)for(let r=0;r<4;r++)o[t][r]=!0;t.push(r(o))}const n=e(4,4);for(let t=0;t<4;t++)for(let r=0;r<4;r++)n[t][r]=!0;t.push(r(n)),t.push(r(n));for(let n=0;n<4;n++){const o=e(4,4);for(let t=n+1;t<4;t++)for(let r=0;r<4;r++)o[t][r]=!0;t.push(r(o))}return t.push(r(e(4,4))),t}(),interval:100},diagswipe:{frames:function(){const t=[];for(let n=0;n<=6;n++){const o=e(4,4);for(let t=0;t<4;t++)for(let r=0;r<4;r++)t+r<=n&&(o[t][r]=!0);t.push(r(o))}const n=e(4,4);for(let t=0;t<4;t++)for(let r=0;r<4;r++)n[t][r]=!0;t.push(r(n));for(let n=0;n<=6;n++){const o=e(4,4);for(let t=0;t<4;t++)for(let r=0;r<4;r++)t+r>n&&(o[t][r]=!0);t.push(r(o))}return t.push(r(e(4,4))),t}(),interval:60}},o=n;export{o as default,r as gridToBraille,e as makeGrid,n as spinners};
8
+ //# sourceMappingURL=/sm/c947d62ba1809e83ffc204ca911a4f2ddec2f0a6cf80bde523151a9fbb52db31.map
b/95d704019ecda0ed07685277867299b1e7cc94c0f2e435c9f047bd5cdeb5cdab ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // holo-linux-tee.mjs β€” device-biometric unlock for the sovereign machine (WebAuthn PRF).
2
+ //
3
+ // Returns a secret gated by the platform authenticator (Touch ID / Windows Hello / the native
4
+ // holo:hello ceremony in CEF) β€” one biometric. The worker HKDFs it into the AES-GCM seal key, so the
5
+ // saved machine is bound to your biometric on this device. The secret NEVER persists (re-derived per
6
+ // unlock). When there is no platform authenticator (e.g. a headless/plain context), unlock returns
7
+ // null and the caller falls back to the soft, device-local key β€” boot is never blocked.
8
+ //
9
+ // TWO entry points, deliberately separate:
10
+ // β€’ teeUnlock(id) β€” GET only. Prompts for one biometric ONLY if this machine is already sovereign
11
+ // (a resident credential exists). A brand-new machine returns null with NO prompt β†’ soft tier.
12
+ // This is what boot calls, so a first-time user is never ambushed by a Windows Hello dialog.
13
+ // β€’ teeEnroll(id) β€” the explicit "make this machine sovereign" action. Creates the resident
14
+ // credential (one biometric) and derives the PRF secret, falling back to a second assertion only
15
+ // on platforms that don't return PRF straight from create(). Behind a user gesture, by design.
16
+ const LS_CRED = (id) => "holoLinuxCred:" + id; // per-machine resident credential id (b64url)
17
+ const RP = { name: "Holo Linux" }; // rpId defaults to the page origin (localhost on the broker)
18
+ const b64u = (b) => btoa(String.fromCharCode(...new Uint8Array(b))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
19
+ const fromB64u = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
20
+ const PRF_SALT = new TextEncoder().encode("holo-linux/machine/prf/v1");
21
+
22
+ export async function teeAvailable() {
23
+ try { return !!(window.PublicKeyCredential && await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()); }
24
+ catch (_) { return false; }
25
+ }
26
+
27
+ // True once this machine has a resident credential = it is sovereign (sealed to the biometric). Boot uses
28
+ // this to decide whether to prompt at all. No crypto, no prompt β€” just the local marker.
29
+ export function isSovereign(id) { return !!localStorage.getItem(LS_CRED(id)); }
30
+
31
+ // One biometric assertion β†’ the per-machine PRF secret (ArrayBuffer), or null if the authenticator has no
32
+ // PRF/hmac-secret. Shared by unlock + enroll's fallback.
33
+ async function getPrf(rawId) {
34
+ const assertion = await navigator.credentials.get({ publicKey: {
35
+ challenge: crypto.getRandomValues(new Uint8Array(32)),
36
+ allowCredentials: [{ id: rawId, type: "public-key" }],
37
+ userVerification: "required",
38
+ extensions: { prf: { eval: { first: PRF_SALT } } },
39
+ } });
40
+ const res = assertion && assertion.getClientExtensionResults && assertion.getClientExtensionResults();
41
+ return (res && res.prf && res.prf.results && res.prf.results.first) || null;
42
+ }
43
+
44
+ // GET-ONLY unlock. Returns the PRF secret if this machine is sovereign, else null (β†’ soft tier, NO prompt).
45
+ // Never enrolls β€” a new machine boots straight to soft without a biometric dialog.
46
+ export async function teeUnlock(id) {
47
+ try {
48
+ if (!(await teeAvailable())) return null;
49
+ const stored = localStorage.getItem(LS_CRED(id));
50
+ if (!stored) return null; // not sovereign yet β†’ soft, no prompt
51
+ return await getPrf(fromB64u(stored)) || null;
52
+ } catch (_) { return null; } // cancelled / unsupported β†’ soft fallback (never breaks boot)
53
+ }
54
+
55
+ // Explicit "make this machine sovereign": enroll a platform resident credential, derive the PRF secret.
56
+ // One biometric on platforms that return PRF from create(); two on those that don't (the create, then a
57
+ // single assertion). Returns the secret, or null if the authenticator can't do PRF (caller stays soft and
58
+ // must NOT treat the machine as sovereign β€” we roll the credential marker back so isSovereign stays false).
59
+ export async function teeEnroll(id) {
60
+ try {
61
+ if (!(await teeAvailable())) return null;
62
+ const cred = await navigator.credentials.create({ publicKey: {
63
+ rp: RP,
64
+ user: { id: new TextEncoder().encode("holo-linux:" + id), name: "holo-linux/" + id, displayName: "Holo Linux machine" },
65
+ challenge: crypto.getRandomValues(new Uint8Array(32)),
66
+ pubKeyCredParams: [{ type: "public-key", alg: -7 }, { type: "public-key", alg: -257 }],
67
+ authenticatorSelection: { residentKey: "required", userVerification: "required", authenticatorAttachment: "platform" },
68
+ extensions: { prf: { eval: { first: PRF_SALT } } },
69
+ } });
70
+ if (!cred) return null;
71
+ localStorage.setItem(LS_CRED(id), b64u(cred.rawId));
72
+ // Fast path: some authenticators (incl. recent Windows Hello) return PRF straight from create β€” one prompt.
73
+ const cr = cred.getClientExtensionResults && cred.getClientExtensionResults();
74
+ let prf = (cr && cr.prf && cr.prf.results && cr.prf.results.first) || null;
75
+ if (!prf) prf = await getPrf(fromB64u(localStorage.getItem(LS_CRED(id)))); // second prompt only if needed
76
+ if (!prf) { localStorage.removeItem(LS_CRED(id)); return null; } // no PRF β†’ roll back; stay soft
77
+ return prf;
78
+ } catch (_) { localStorage.removeItem(LS_CRED(id)); return null; } // cancelled β†’ roll back; stay soft
79
+ }
b/996b3aa91392ca5e20a66924127be48eb5f804ad1ca48525f9363ff010307427 ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # persist β€” durable serverless state. Your work lives on a real ext4 ΞΊ-disk inside the browser
3
+ # (OPFS) β€” no server, no cloud. Write now, press ⟳ reboot, run `persist` again: still here.
4
+
5
+ A=$'\033[1;38;5;51m'; D=$'\033[38;5;244m'; G=$'\033[38;5;72m'; Y=$'\033[38;5;220m'; R=$'\033[0m'
6
+ f=/workspace/.persist
7
+
8
+ printf '\n%sHolo Linux persists β€” with no server%s\n\n' "$A" "$R"
9
+
10
+ if [ -f "$f" ]; then
11
+ printf '%snotes already on the ΞΊ-disk from earlier boots:%s\n' "$G" "$R"
12
+ sed 's/^/ /' "$f"
13
+ printf '\n%sThose survived a reboot with no server in the loop.%s\n' "$Y" "$R"
14
+ else
15
+ printf '%sno note yet β€” writing the first one to the ΞΊ-disk…%s\n' "$D" "$R"
16
+ fi
17
+
18
+ d=$(date -u '+%Y-%m-%d %H:%M:%SZ' 2>/dev/null); [ -z "$d" ] && d="boot+$(awk '{printf "%.0fs", $1}' /proc/uptime 2>/dev/null)"
19
+ printf 'note Β· %s\n' "$d" >> "$f"
20
+ sync
21
+
22
+ printf '\n%snow on disk%s %s\n' "$G" "$R" "$f"
23
+ df -h / | sed 's/^/ /'
24
+
25
+ printf '\n%sPress ⟳ reboot and run persist again β€” your note is still here. A real,\n' "$D"
26
+ printf 'content-addressed ext4 disk in your browser; nothing was sent anywhere.%s\n\n' "$R"
b/a0cf7e001e6b509301f709a952fe2f5073b95ba618dd991874ef26aa85d8891d ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # verify β€” prove this is a real Linux kernel, not a scripted shell. Every check below reads
3
+ # state the kernel SYNTHESISES at runtime (/proc, the ring buffer, the CSPRNG, the VFS).
4
+ # A command->string mock cannot reproduce any of it. Run it, read the "why", check it yourself.
5
+
6
+ A=$'\033[1;38;5;51m'; D=$'\033[38;5;244m'; Y=$'\033[38;5;220m'; R=$'\033[0m'
7
+ step() { printf '\n%s== %s%s\n%s# %s%s\n' "$A" "$1" "$R" "$Y" "$2" "$R"; eval "$2"; }
8
+ why() { printf '%s why: %s%s\n' "$D" "$1" "$R"; }
9
+
10
+ printf '\n%sHolo Linux self-verification%s\n' "$A" "$R"
11
+
12
+ why "release, arch and SMP build string come straight from the running kernel"
13
+ step "kernel identity" "uname -a"
14
+
15
+ why "/proc/cpuinfo is generated by the kernel β€” real RISC-V harts, ISA extensions, MMU mode"
16
+ step "the CPU" "grep -E 'processor|isa|mmu|hart' /proc/cpuinfo"
17
+
18
+ why "real PIDs in a real process table; PID 1 is init β€” no /proc fakery possible"
19
+ step "process table (from /proc, no ps needed)" "ls -d /proc/[0-9]* | sed 's#/proc/##' | tr '\n' ' '; echo; echo -n 'PID1 = '; cat /proc/1/comm"
20
+
21
+ why "the kernel's own boot ring buffer β€” a script has no dmesg to show"
22
+ step "kernel ring buffer" "dmesg | tail -6"
23
+
24
+ why "real VFS: an ext4 root on the virtio block device, plus proc/sysfs/devtmpfs"
25
+ step "mounted filesystems" "mount | grep -E 'ext4|proc|sysfs|devtmpfs'"
26
+
27
+ why "two reads of the kernel CSPRNG give two different UUIDs β€” not a constant"
28
+ step "kernel entropy" "cat /proc/sys/kernel/random/uuid; cat /proc/sys/kernel/random/uuid"
29
+
30
+ why "write to the real ext4, sync, hash it back β€” bytes round-trip through a real fs"
31
+ step "filesystem round-trip" "head -c 4096 /dev/urandom > /tmp/holo.bin && sync && sha256sum /tmp/holo.bin && ls -l /tmp/holo.bin"
32
+
33
+ printf '\n%sAll output above was produced by the kernel/userland in this tab. Nothing was scripted.%s\n\n' "$A" "$R"
b/ac97ac99cb90425d4fe91bcc20c7149e043a2574f62999c79d28ca8515229376 ADDED
The diff for this file is too large to render. See raw diff
 
b/b287e4c57be0b52c3182bf43edc16bda29a1dcdf0d5ec2d486e1944f8f13d654 ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.WebLinksAddon=t():e.WebLinksAddon=t()}(self,(()=>(()=>{"use strict";var e={6:(e,t)=>{function n(e){try{const t=new URL(e),n=t.password&&t.username?`${t.protocol}//${t.username}:${t.password}@${t.host}`:t.username?`${t.protocol}//${t.username}@${t.host}`:`${t.protocol}//${t.host}`;return e.toLocaleLowerCase().startsWith(n.toLocaleLowerCase())}catch(e){return!1}}Object.defineProperty(t,"__esModule",{value:!0}),t.LinkComputer=t.WebLinkProvider=void 0,t.WebLinkProvider=class{constructor(e,t,n,o={}){this._terminal=e,this._regex=t,this._handler=n,this._options=o}provideLinks(e,t){const n=o.computeLink(e,this._regex,this._terminal,this._handler);t(this._addCallbacks(n))}_addCallbacks(e){return e.map((e=>(e.leave=this._options.leave,e.hover=(t,n)=>{if(this._options.hover){const{range:o}=e;this._options.hover(t,n,o)}},e)))}};class o{static computeLink(e,t,r,i){const s=new RegExp(t.source,(t.flags||"")+"g"),[a,c]=o._getWindowedLineStrings(e-1,r),l=a.join("");let d;const p=[];for(;d=s.exec(l);){const e=d[0];if(!n(e))continue;const[t,s]=o._mapStrIdx(r,c,0,d.index),[a,l]=o._mapStrIdx(r,t,s,e.length);if(-1===t||-1===s||-1===a||-1===l)continue;const h={start:{x:s+1,y:t+1},end:{x:l,y:a+1}};p.push({range:h,text:e,activate:i})}return p}static _getWindowedLineStrings(e,t){let n,o=e,r=e,i=0,s="";const a=[];if(n=t.buffer.active.getLine(e)){const e=n.translateToString(!0);if(n.isWrapped&&" "!==e[0]){for(i=0;(n=t.buffer.active.getLine(--o))&&i<2048&&(s=n.translateToString(!0),i+=s.length,a.push(s),n.isWrapped&&-1===s.indexOf(" ")););a.reverse()}for(a.push(e),i=0;(n=t.buffer.active.getLine(++r))&&n.isWrapped&&i<2048&&(s=n.translateToString(!0),i+=s.length,a.push(s),-1===s.indexOf(" ")););}return[a,o]}static _mapStrIdx(e,t,n,o){const r=e.buffer.active,i=r.getNullCell();let s=n;for(;o;){const e=r.getLine(t);if(!e)return[-1,-1];for(let n=s;n<e.length;++n){e.getCell(n,i);const s=i.getChars();if(i.getWidth()&&(o-=s.length||1,n===e.length-1&&""===s)){const e=r.getLine(t+1);e&&e.isWrapped&&(e.getCell(0,i),2===i.getWidth()&&(o+=1))}if(o<0)return[t,n]}t++,s=0}return[t,s]}}t.LinkComputer=o}},t={};function n(o){var r=t[o];if(void 0!==r)return r.exports;var i=t[o]={exports:{}};return e[o](i,i.exports,n),i.exports}var o={};return(()=>{var e=o;Object.defineProperty(e,"__esModule",{value:!0}),e.WebLinksAddon=void 0;const t=n(6),r=/(https?|HTTPS?):[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;function i(e,t){const n=window.open();if(n){try{n.opener=null}catch{}n.location.href=t}else console.warn("Opening link blocked as opener could not be cleared")}e.WebLinksAddon=class{constructor(e=i,t={}){this._handler=e,this._options=t}activate(e){this._terminal=e;const n=this._options,o=n.urlRegex||r;this._linkProvider=this._terminal.registerLinkProvider(new t.WebLinkProvider(this._terminal,o,this._handler,n))}dispose(){this._linkProvider?.dispose()}}})(),o})()));
2
+ //# sourceMappingURL=addon-web-links.js.map
b/b2bb781c8c15f4717080e7bde1195d79eb1363dd734633c630f4c587670eb884 ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # help β€” what can I actually run here? Probes the LIVE guest with `command -v`, so it can only
3
+ # ever print commands that truly exist in this rootfs. The guest is the source of truth.
4
+
5
+ A=$'\033[1;38;5;51m'; D=$'\033[38;5;244m'; G=$'\033[38;5;72m'; R=$'\033[0m'
6
+ have() { command -v "$1" >/dev/null 2>&1; }
7
+ row() { local title=$1; shift; local out="" c; for c in "$@"; do have "$c" && out="$out $c"; done; [ -n "$out" ] && printf ' %s%-9s%s%s\n' "$G" "$title" "$R" "$out"; }
8
+
9
+ printf '\n%sHolo Linux β€” what really runs here%s\n\n' "$A" "$R"
10
+ printf ' %sthe substrate, made tangible:%s\n' "$D" "$R"
11
+ printf ' %s%-13s%s prove this is a real Linux kernel\n' "$A" "verify" "$R"
12
+ printf ' %s%-13s%s no server β€” the whole OS runs in this tab\n' "$A" "serverless" "$R"
13
+ printf ' %s%-13s%s every byte has a content address (ΞΊ)\n' "$A" "fingerprint" "$R"
14
+ printf ' %s%-13s%s flip one byte and it will not boot\n' "$A" "tamper" "$R"
15
+ printf ' %s%-13s%s your work survives a reboot, with no server\n' "$A" "persist" "$R"
16
+ printf ' %s%-13s%s rebuild this exact machine from a hash\n' "$A" "reproduce" "$R"
17
+
18
+ printf '\n %snative Debian commands present in this rootfs:%s\n' "$D" "$R"
19
+ row "files" ls cp mv rm ln mkdir rmdir pwd stat readlink find du tree
20
+ row "view" cat head tail more tac nl strings file
21
+ row "text" grep egrep sed awk sort uniq tr tee sha256sum md5sum base64 diff xargs
22
+ row "process" sh bash kill nice nohup sleep timeout env id whoami uptime
23
+ row "system" uname hostname dmesg date sync mount df nproc
24
+ row "archive" tar gzip gunzip zcat xz bzip2 cpio
25
+
26
+ printf '\n %sNo compiler, editor, or network stack is installed β€” help only lists what is\n' "$D"
27
+ printf ' really here, so the absence is honest too.%s\n\n' "$R"
b/c2fbf3e19ed81c6b5ad45c60b95289e5b483789861f7bd277ca99bd85897047f ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) 2014 The xterm.js authors. All rights reserved.
3
+ * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
4
+ * https://github.com/chjj/term.js
5
+ * @license MIT
6
+ *
7
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ * of this software and associated documentation files (the "Software"), to deal
9
+ * in the Software without restriction, including without limitation the rights
10
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ * copies of the Software, and to permit persons to whom the Software is
12
+ * furnished to do so, subject to the following conditions:
13
+ *
14
+ * The above copyright notice and this permission notice shall be included in
15
+ * all copies or substantial portions of the Software.
16
+ *
17
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ * THE SOFTWARE.
24
+ *
25
+ * Originally forked from (with the author's permission):
26
+ * Fabrice Bellard's javascript vt100 for jslinux:
27
+ * http://bellard.org/jslinux/
28
+ * Copyright (c) 2011 Fabrice Bellard
29
+ * The original design remains. The terminal itself
30
+ * has been extended to include xterm CSI codes, among
31
+ * other features.
32
+ */
33
+
34
+ /**
35
+ * Default styles for xterm.js
36
+ */
37
+
38
+ .xterm {
39
+ cursor: text;
40
+ position: relative;
41
+ user-select: none;
42
+ -ms-user-select: none;
43
+ -webkit-user-select: none;
44
+ }
45
+
46
+ .xterm.focus,
47
+ .xterm:focus {
48
+ outline: none;
49
+ }
50
+
51
+ .xterm .xterm-helpers {
52
+ position: absolute;
53
+ top: 0;
54
+ /**
55
+ * The z-index of the helpers must be higher than the canvases in order for
56
+ * IMEs to appear on top.
57
+ */
58
+ z-index: 5;
59
+ }
60
+
61
+ .xterm .xterm-helper-textarea {
62
+ padding: 0;
63
+ border: 0;
64
+ margin: 0;
65
+ /* Move textarea out of the screen to the far left, so that the cursor is not visible */
66
+ position: absolute;
67
+ opacity: 0;
68
+ left: -9999em;
69
+ top: 0;
70
+ width: 0;
71
+ height: 0;
72
+ z-index: -5;
73
+ /** Prevent wrapping so the IME appears against the textarea at the correct position */
74
+ white-space: nowrap;
75
+ overflow: hidden;
76
+ resize: none;
77
+ }
78
+
79
+ .xterm .composition-view {
80
+ /* TODO: Composition position got messed up somewhere */
81
+ background: #000;
82
+ color: #FFF;
83
+ display: none;
84
+ position: absolute;
85
+ white-space: nowrap;
86
+ z-index: 1;
87
+ }
88
+
89
+ .xterm .composition-view.active {
90
+ display: block;
91
+ }
92
+
93
+ .xterm .xterm-viewport {
94
+ /* On OS X this is required in order for the scroll bar to appear fully opaque */
95
+ background-color: #000;
96
+ overflow-y: scroll;
97
+ cursor: default;
98
+ position: absolute;
99
+ right: 0;
100
+ left: 0;
101
+ top: 0;
102
+ bottom: 0;
103
+ }
104
+
105
+ .xterm .xterm-screen {
106
+ position: relative;
107
+ }
108
+
109
+ .xterm .xterm-screen canvas {
110
+ position: absolute;
111
+ left: 0;
112
+ top: 0;
113
+ }
114
+
115
+ .xterm .xterm-scroll-area {
116
+ visibility: hidden;
117
+ }
118
+
119
+ .xterm-char-measure-element {
120
+ display: inline-block;
121
+ visibility: hidden;
122
+ position: absolute;
123
+ top: 0;
124
+ left: -9999em;
125
+ line-height: normal;
126
+ }
127
+
128
+ .xterm.enable-mouse-events {
129
+ /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
130
+ cursor: default;
131
+ }
132
+
133
+ .xterm.xterm-cursor-pointer,
134
+ .xterm .xterm-cursor-pointer {
135
+ cursor: pointer;
136
+ }
137
+
138
+ .xterm.column-select.focus {
139
+ /* Column selection mode */
140
+ cursor: crosshair;
141
+ }
142
+
143
+ .xterm .xterm-accessibility:not(.debug),
144
+ .xterm .xterm-message {
145
+ position: absolute;
146
+ left: 0;
147
+ top: 0;
148
+ bottom: 0;
149
+ right: 0;
150
+ z-index: 10;
151
+ color: transparent;
152
+ pointer-events: none;
153
+ }
154
+
155
+ .xterm .xterm-accessibility-tree:not(.debug) *::selection {
156
+ color: transparent;
157
+ }
158
+
159
+ .xterm .xterm-accessibility-tree {
160
+ user-select: text;
161
+ white-space: pre;
162
+ }
163
+
164
+ .xterm .live-region {
165
+ position: absolute;
166
+ left: -9999px;
167
+ width: 1px;
168
+ height: 1px;
169
+ overflow: hidden;
170
+ }
171
+
172
+ .xterm-dim {
173
+ /* Dim should not apply to background, so the opacity of the foreground color is applied
174
+ * explicitly in the generated class and reset to 1 here */
175
+ opacity: 1 !important;
176
+ }
177
+
178
+ .xterm-underline-1 { text-decoration: underline; }
179
+ .xterm-underline-2 { text-decoration: double underline; }
180
+ .xterm-underline-3 { text-decoration: wavy underline; }
181
+ .xterm-underline-4 { text-decoration: dotted underline; }
182
+ .xterm-underline-5 { text-decoration: dashed underline; }
183
+
184
+ .xterm-overline {
185
+ text-decoration: overline;
186
+ }
187
+
188
+ .xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
189
+ .xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
190
+ .xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
191
+ .xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
192
+ .xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
193
+
194
+ .xterm-strikethrough {
195
+ text-decoration: line-through;
196
+ }
197
+
198
+ .xterm-screen .xterm-decoration-container .xterm-decoration {
199
+ z-index: 6;
200
+ position: absolute;
201
+ }
202
+
203
+ .xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
204
+ z-index: 7;
205
+ }
206
+
207
+ .xterm-decoration-overview-ruler {
208
+ z-index: 8;
209
+ position: absolute;
210
+ top: 0;
211
+ right: 0;
212
+ pointer-events: none;
213
+ }
214
+
215
+ .xterm-decoration-top {
216
+ z-index: 2;
217
+ position: relative;
218
+ }
b/c50bbb654dc8ebf24e9ef81a8d187a66de48a1cff620cf54fb2ff8f74832f799 ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # tamper β€” integrity by construction: change ONE byte and the content address changes utterly.
3
+ # That is exactly why the ΞΊ-gate refuses a corrupted kernel or rootfs. Shown live with two
4
+ # tiny files that differ by a single byte β€” not asserted, demonstrated.
5
+
6
+ A=$'\033[1;38;5;51m'; D=$'\033[38;5;244m'; B=$'\033[38;5;39m'; Y=$'\033[38;5;220m'; R=$'\033[0m'
7
+
8
+ printf '\n%sHolo Linux is tamper-evident%s\n\n' "$A" "$R"
9
+
10
+ printf 'HOLO-LINUX' > /tmp/orig.bin
11
+ printf 'XOLO-LINUX' > /tmp/bent.bin # byte-for-byte identical but for the very first character
12
+ o=$(sha256sum /tmp/orig.bin | awk '{print $1}')
13
+ b=$(sha256sum /tmp/bent.bin | awk '{print $1}')
14
+
15
+ printf ' %sHOLO-LINUX%s β†’ %s\n' "$B" "$R" "$o"
16
+ printf ' %sXOLO-LINUX%s β†’ %s\n' "$B" "$R" "$b"
17
+
18
+ printf '\n%sOne byte changed; the entire ΞΊ changed.%s\n' "$Y" "$R"
19
+ printf '%sThe boot re-derives the kernel + rootfs ΞΊ and refuses any mismatch. Press the\n' "$D"
20
+ printf '⚠ tamper test in the toolbar to watch a single flipped rootfs byte stop the\n'
21
+ printf 'machine cold β€” a corrupted download simply cannot run here.%s\n\n' "$R"
b/cbbef78890e83aa015dca82a2d6d170e5865de80c57fd45dcd48535e0d90d0de ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardAddon=e():t.ClipboardAddon=e()}(self,(()=>(()=>{var t={575:function(t,e,r){"undefined"!=typeof self?self:"undefined"!=typeof window?window:void 0!==r.g&&r.g,t.exports=function(){"use strict";var t,e="3.7.7",r=e,n="function"==typeof Buffer,o="function"==typeof TextDecoder?new TextDecoder:void 0,i="function"==typeof TextEncoder?new TextEncoder:void 0,u=Array.prototype.slice.call("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="),a=(t={},u.forEach((function(e,r){return t[e]=r})),t),c=/^(?:[A-Za-z\d+\/]{4})*?(?:[A-Za-z\d+\/]{2}(?:==)?|[A-Za-z\d+\/]{3}=?)?$/,f=String.fromCharCode.bind(String),s="function"==typeof Uint8Array.from?Uint8Array.from.bind(Uint8Array):function(t){return new Uint8Array(Array.prototype.slice.call(t,0))},d=function(t){return t.replace(/=/g,"").replace(/[+\/]/g,(function(t){return"+"==t?"-":"_"}))},l=function(t){return t.replace(/[^A-Za-z0-9\+\/]/g,"")},p=function(t){for(var e,r,n,o,i="",a=t.length%3,c=0;c<t.length;){if((r=t.charCodeAt(c++))>255||(n=t.charCodeAt(c++))>255||(o=t.charCodeAt(c++))>255)throw new TypeError("invalid character found");i+=u[(e=r<<16|n<<8|o)>>18&63]+u[e>>12&63]+u[e>>6&63]+u[63&e]}return a?i.slice(0,a-3)+"===".substring(a):i},h="function"==typeof btoa?function(t){return btoa(t)}:n?function(t){return Buffer.from(t,"binary").toString("base64")}:p,b=n?function(t){return Buffer.from(t).toString("base64")}:function(t){for(var e=[],r=0,n=t.length;r<n;r+=4096)e.push(f.apply(null,t.subarray(r,r+4096)));return h(e.join(""))},y=function(t,e){return void 0===e&&(e=!1),e?d(b(t)):b(t)},x=function(t){if(t.length<2)return(e=t.charCodeAt(0))<128?t:e<2048?f(192|e>>>6)+f(128|63&e):f(224|e>>>12&15)+f(128|e>>>6&63)+f(128|63&e);var e=65536+1024*(t.charCodeAt(0)-55296)+(t.charCodeAt(1)-56320);return f(240|e>>>18&7)+f(128|e>>>12&63)+f(128|e>>>6&63)+f(128|63&e)},A=/[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g,g=function(t){return t.replace(A,x)},v=n?function(t){return Buffer.from(t,"utf8").toString("base64")}:i?function(t){return b(i.encode(t))}:function(t){return h(g(t))},B=function(t,e){return void 0===e&&(e=!1),e?d(v(t)):v(t)},C=function(t){return B(t,!0)},m=/[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/g,w=function(t){switch(t.length){case 4:var e=((7&t.charCodeAt(0))<<18|(63&t.charCodeAt(1))<<12|(63&t.charCodeAt(2))<<6|63&t.charCodeAt(3))-65536;return f(55296+(e>>>10))+f(56320+(1023&e));case 3:return f((15&t.charCodeAt(0))<<12|(63&t.charCodeAt(1))<<6|63&t.charCodeAt(2));default:return f((31&t.charCodeAt(0))<<6|63&t.charCodeAt(1))}},T=function(t){return t.replace(m,w)},_=function(t){if(t=t.replace(/\s+/g,""),!c.test(t))throw new TypeError("malformed base64.");t+="==".slice(2-(3&t.length));for(var e,r,n,o="",i=0;i<t.length;)e=a[t.charAt(i++)]<<18|a[t.charAt(i++)]<<12|(r=a[t.charAt(i++)])<<6|(n=a[t.charAt(i++)]),o+=64===r?f(e>>16&255):64===n?f(e>>16&255,e>>8&255):f(e>>16&255,e>>8&255,255&e);return o},F="function"==typeof atob?function(t){return atob(l(t))}:n?function(t){return Buffer.from(t,"base64").toString("binary")}:_,U=n?function(t){return s(Buffer.from(t,"base64"))}:function(t){return s(F(t).split("").map((function(t){return t.charCodeAt(0)})))},P=function(t){return U(S(t))},j=n?function(t){return Buffer.from(t,"base64").toString("utf8")}:o?function(t){return o.decode(U(t))}:function(t){return T(F(t))},S=function(t){return l(t.replace(/[-_]/g,(function(t){return"-"==t?"+":"/"})))},E=function(t){return j(S(t))},R=function(t){return{value:t,enumerable:!1,writable:!0,configurable:!0}},O=function(){var t=function(t,e){return Object.defineProperty(String.prototype,t,R(e))};t("fromBase64",(function(){return E(this)})),t("toBase64",(function(t){return B(this,t)})),t("toBase64URI",(function(){return B(this,!0)})),t("toBase64URL",(function(){return B(this,!0)})),t("toUint8Array",(function(){return P(this)}))},D=function(){var t=function(t,e){return Object.defineProperty(Uint8Array.prototype,t,R(e))};t("toBase64",(function(t){return y(this,t)})),t("toBase64URI",(function(){return y(this,!0)})),t("toBase64URL",(function(){return y(this,!0)}))},z={version:e,VERSION:r,atob:F,atobPolyfill:_,btoa:h,btoaPolyfill:p,fromBase64:E,toBase64:B,encode:B,encodeURI:C,encodeURL:C,utob:g,btou:T,decode:E,isValid:function(t){if("string"!=typeof t)return!1;var e=t.replace(/\s+/g,"").replace(/={0,2}$/,"");return!/[^\s0-9a-zA-Z\+/]/.test(e)||!/[^\s0-9a-zA-Z\-_]/.test(e)},fromUint8Array:y,toUint8Array:P,extendString:O,extendUint8Array:D,extendBuiltins:function(){O(),D()},Base64:{}};return Object.keys(z).forEach((function(t){return z.Base64[t]=z[t]})),z}()}},e={};function r(n){var o=e[n];if(void 0!==o)return o.exports;var i=e[n]={exports:{}};return t[n].call(i.exports,i,i.exports,r),i.exports}r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}();var n={};return(()=>{"use strict";var t=n;Object.defineProperty(t,"__esModule",{value:!0}),t.Base64=t.BrowserClipboardProvider=t.ClipboardAddon=void 0;const e=r(575);t.ClipboardAddon=class{constructor(t=new i,e=new o){this._base64=t,this._provider=e}activate(t){this._terminal=t,this._disposable=t.parser.registerOscHandler(52,(t=>this._setOrReportClipboard(t)))}dispose(){return this._disposable?.dispose()}_readText(t,e){const r=this._base64.encodeText(e);this._terminal?.input(`]52;${t};${r}`,!1)}_setOrReportClipboard(t){const e=t.split(";");if(e.length<2)return!0;const r=e[0],n=e[1];if("?"===n){const t=this._provider.readText(r);return t instanceof Promise?t.then((t=>(this._readText(r,t),!0))):(this._readText(r,t),!0)}let o="";try{o=this._base64.decodeText(n)}catch{}const i=this._provider.writeText(r,o);return!(i instanceof Promise)||i.then((()=>!0))}};class o{async readText(t){return"c"!==t?Promise.resolve(""):navigator.clipboard.readText()}async writeText(t,e){return"c"!==t?Promise.resolve():navigator.clipboard.writeText(e)}}t.BrowserClipboardProvider=o;class i{encodeText(t){return e.Base64.encode(t)}decodeText(t){const r=e.Base64.decode(t);return e.Base64.isValid(t)&&e.Base64.encode(r)===t?r:""}}t.Base64=i})(),n})()));
2
+ //# sourceMappingURL=addon-clipboard.js.map
b/cdd0afa46a7402609a1dd436aa41cafeb01615340e25423a2af8e9ddc7c4026b ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # /etc/profile.d/00-holo-linux.sh β€” Holo Linux login banner.
2
+ # Runs from /etc/profile's run-parts loop for every login shell. Everything printed
3
+ # here is produced BY THE GUEST at login: uname, /etc/os-release, /proc β€” no host
4
+ # text is injected. Guarded so it shows once per boot, not on every subshell.
5
+
6
+ # A real interactive terminal: announce 256-color + UTF-8 so colour/box-drawing work.
7
+ case "$TERM" in
8
+ xterm | xterm-color | "" ) export TERM=xterm-256color ;;
9
+ esac
10
+
11
+ # Give the machine a name (kernel hostname was "(none)"); no binary needed, just procfs.
12
+ if [ "$(cat /proc/sys/kernel/hostname 2>/dev/null)" = "(none)" ] && [ -w /proc/sys/kernel/hostname ]; then
13
+ echo holo > /proc/sys/kernel/hostname 2>/dev/null || true
14
+ fi
15
+
16
+ # A real, colour prompt. bash captures the hostname for \h ONCE at startup, so \h
17
+ # would still print the old "(none)"; read the live hostname from procfs instead.
18
+ if [ -n "$PS1" ]; then
19
+ _hn=$(cat /proc/sys/kernel/hostname 2>/dev/null); [ -z "$_hn" ] || [ "$_hn" = "(none)" ] && _hn=holo
20
+ PS1='\[\033[01;32m\]\u@'"$_hn"'\[\033[0m\]:\[\033[01;34m\]\w\[\033[0m\]\$ '
21
+ # `help` is a bash builtin; alias it so the banner's command runs OUR script (aliases win).
22
+ alias help='/usr/local/bin/help'
23
+ fi
24
+
25
+ # Show the banner once per boot (not for nested login shells).
26
+ if [ -n "$PS1" ] && [ -z "$HOLO_MOTD_SHOWN" ]; then
27
+ export HOLO_MOTD_SHOWN=1
28
+
29
+ _krel=$(uname -r 2>/dev/null); _arch=$(uname -m 2>/dev/null)
30
+ _distro=$(. /etc/os-release 2>/dev/null; printf '%s' "$PRETTY_NAME")
31
+ _cpus=$(nproc 2>/dev/null)
32
+ _isa=$(awk -F': ' '/^isa/{print $2; exit}' /proc/cpuinfo 2>/dev/null)
33
+ _mmu=$(awk -F': ' '/^mmu/{print $2; exit}' /proc/cpuinfo 2>/dev/null)
34
+ _memkb=$(awk '/^MemTotal/{print $2; exit}' /proc/meminfo 2>/dev/null)
35
+ _memmb=$(( ${_memkb:-0} / 1024 ))
36
+ _up=$(awk '{printf "%d.%02ds", $1, ($1-int($1))*100; exit}' /proc/uptime 2>/dev/null)
37
+
38
+ # ANSI: 39=tux body, 220=beak/feet, 244=dim, 51=accent
39
+ B=$'\033[38;5;39m'; Y=$'\033[38;5;220m'; D=$'\033[38;5;244m'; A=$'\033[1;38;5;51m'; R=$'\033[0m'
40
+
41
+ printf '\n'
42
+ printf ' %s.--.%s %sHolo Linux%s\n' "$B" "$R" "$A" "$R"
43
+ printf ' %s|o_o |%s %sA real %s Linux, ΞΊ-verified and booted in a browser tab.%s\n' "$B" "$R" "$D" "$_arch" "$R"
44
+ printf ' %s|:_/ |%s\n' "$B" "$R"
45
+ printf ' %s// \\ \\%s %skernel %s %s%s\n' "$B" "$R" "$D" "$R" "$B" "$_krel"
46
+ printf '%s(| | )%s %sdistro %s %s%s\n' "$B" "$R" "$D" "$R" "$B" "${_distro:-Debian}"
47
+ printf '%s/'"'"'\\_ _/`\\%s %scpu %s %s%s hart Β· %s%s\n' "$B" "$R" "$D" "$R" "$B" "${_cpus:-1}" "${_isa:-riscv64}" "$R"
48
+ printf '%s\\___)=(___/%s %smemory %s %s%s MiB Β· mmu %s%s\n' "$B" "$R" "$D" "$R" "$B" "$_memmb" "${_mmu:-sv57}" "$R"
49
+ printf ' %suptime %s %s%s%s\n' "$D" "$R" "$B" "${_up:-0s}" "$R"
50
+ printf '\n'
51
+ printf ' %sAnchored in the ΞΊ-substrate β€” kernel + rootfs admitted by content\n' "$D"
52
+ printf ' address, not by trust (Law L5). Tamper one byte and the boot refuses.%s\n' "$R"
53
+ printf '\n'
54
+ printf ' %s%-13s%s what can I run here\n' "$A" "help" "$R"
55
+ printf ' %s%-13s%s prove this is a real Linux kernel\n' "$A" "verify" "$R"
56
+ printf ' %s%-13s%s no server β€” the whole OS runs in this tab\n' "$A" "serverless" "$R"
57
+ printf ' %s%-13s%s every byte has a content address (ΞΊ)\n' "$A" "fingerprint" "$R"
58
+ printf ' %s%-13s%s flip one byte and it will not boot\n' "$A" "tamper" "$R"
59
+ printf ' %s%-13s%s your work survives a reboot, no server\n' "$A" "persist" "$R"
60
+ printf ' %s%-13s%s rebuild this exact machine from a hash\n' "$A" "reproduce" "$R"
61
+ printf '\n'
62
+ fi
b/dcbe5d1951a6b28a0e69ac955073ba55891275b754ddbb232cf12bfbf9bf1cde ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" fill="none" stroke="currentColor" stroke-width="6" stroke-linejoin="round" stroke-linecap="round">
2
+ <rect x="14" y="22" width="100" height="76" rx="9"/><path d="M14 42 H114"/>
3
+ <path d="M30 62 L44 74 L30 86 M56 88 H82"/>
4
+ <circle cx="64" cy="112" r="3.2" fill="currentColor" stroke="none"/><path d="M44 112 H84" opacity="0.5"/>
5
+ </svg>
b/dd131ed675da2a674042c42fc01402d48a323da675371368e89a4342cb02ccb1 ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // holo-linux-sw.js β€” Holo Linux's offline engine. Two jobs:
2
+ //
3
+ // 1. Cross-origin isolation. Stamp COOP/COEP/CORP on every response so the engine's
4
+ // SharedArrayBuffer works even on a static host that sends no such headers.
5
+ // 2. 100% serverless after first load. Cache the WHOLE app β€” shell, engine wasm,
6
+ // the ΞΊ-pinned kernel + Debian rootfs β€” so it boots with the origin server OFF.
7
+ //
8
+ // Every app asset is vendored under this SW's scope, so the SW can serve every byte
9
+ // the boot needs from Cache Storage. The boot's ΞΊ-gate still re-derives the kernel +
10
+ // rootfs SHA-256 on each boot, so serving them from cache is safe: a corrupted cache
11
+ // entry fails the gate exactly like a corrupted download β€” integrity is never trusted.
12
+
13
+ const VERSION = "hl-v4"; // bumped: rootfs re-pinned (7 unprefixed commands) β†’ purge the old cached image
14
+ const CACHE = "holo-linux-" + VERSION;
15
+ const SCOPE = new URL(self.registration.scope).pathname; // e.g. /apps/holo-linux/
16
+
17
+ const COI = {
18
+ "Cross-Origin-Opener-Policy": "same-origin",
19
+ "Cross-Origin-Embedder-Policy": "credentialless",
20
+ "Cross-Origin-Resource-Policy": "cross-origin",
21
+ };
22
+
23
+ // The complete set needed to cold-boot offline (relative to scope). The "make offline"
24
+ // button warms exactly this; the status check reports how much of it is resident.
25
+ const BOOT_SET = [
26
+ "index.html", "holo-linux-worker.js", "holo-splash.js", "unicode-animations.js", "kappa.json", "icon.svg",
27
+ "os-kernel.gz", "os-rootfs.tar.gz",
28
+ "pkg/holospaces_web.js", "pkg/holospaces_web_bg.wasm",
29
+ "vendor/xterm/xterm.css", "vendor/xterm/xterm.js", "vendor/xterm/addon-fit.js",
30
+ "vendor/xterm/addon-webgl.js", "vendor/xterm/addon-web-links.js", "vendor/xterm/addon-search.js",
31
+ "vendor/xterm/addon-unicode11.js", "vendor/xterm/addon-clipboard.js",
32
+ ];
33
+
34
+ // "Airplane mode" β€” when set, the SW refuses ALL network and serves cache only. It
35
+ // makes the serverless claim self-evident: block the network at the worker boundary,
36
+ // cold-boot, and if Linux still comes up, the boot used zero network by construction.
37
+ let NET_BLOCKED = false;
38
+ const netFetch = (req) => NET_BLOCKED ? Promise.reject(new Error("holo: network blocked (airplane mode)")) : fetch(req);
39
+
40
+ self.addEventListener("install", () => self.skipWaiting());
41
+ self.addEventListener("activate", (e) => e.waitUntil((async () => {
42
+ for (const k of await caches.keys()) if (k.startsWith("holo-linux-") && k !== CACHE) await caches.delete(k);
43
+ await self.clients.claim();
44
+ })()));
45
+
46
+ // Rebuild a response with COI headers added (a live network response β€” body is a stream).
47
+ function withCOI(resp) {
48
+ const h = new Headers(resp.headers);
49
+ for (const [k, v] of Object.entries(COI)) h.set(k, v);
50
+ return new Response(resp.body, { status: resp.status, statusText: resp.statusText, headers: h });
51
+ }
52
+ // Same, for a cached response (read its bytes so the result is independently servable).
53
+ async function coiFrom(resp) {
54
+ const buf = await resp.arrayBuffer();
55
+ const h = new Headers(resp.headers);
56
+ for (const [k, v] of Object.entries(COI)) h.set(k, v);
57
+ return new Response(buf, { status: 200, headers: h });
58
+ }
59
+
60
+ // Heavy, content-stable artifacts β†’ cache-first (never re-download 34 MB per boot; the
61
+ // ΞΊ-gate re-verifies them regardless). Everything else β†’ network-first so a live edit
62
+ // shows immediately, with cache as the OFFLINE fallback when the origin is gone.
63
+ const HEAVY = /\/(os-kernel\.gz|os-rootfs\.tar\.gz|holospaces_web_bg\.wasm)(\?|$)/;
64
+ const VENDORED = (p) => p.includes("/vendor/") || p.includes("/pkg/");
65
+
66
+ self.addEventListener("fetch", (event) => {
67
+ const req = event.request;
68
+ if (req.method !== "GET") return;
69
+ const url = new URL(req.url);
70
+ if (url.origin !== self.location.origin || !url.pathname.startsWith(SCOPE)) return;
71
+ event.respondWith(handle(req, url));
72
+ });
73
+
74
+ async function handle(req, url) {
75
+ const cache = await caches.open(CACHE);
76
+ const cacheFirst = HEAVY.test(url.pathname) || VENDORED(url.pathname);
77
+
78
+ if (cacheFirst) {
79
+ const hit = await cache.match(req, { ignoreSearch: true });
80
+ if (hit) return coiFrom(hit);
81
+ try {
82
+ const resp = await netFetch(req);
83
+ if (resp.ok) cache.put(req, resp.clone());
84
+ return withCOI(resp);
85
+ } catch (e) {
86
+ const any = await cache.match(req, { ignoreSearch: true });
87
+ if (any) return coiFrom(any);
88
+ throw e;
89
+ }
90
+ }
91
+
92
+ // network-first (shell / worker / json / small assets) β€” but cache-first while
93
+ // airplane mode is on, so a blocked boot serves entirely from Cache Storage.
94
+ if (NET_BLOCKED) {
95
+ const hit = (await cache.match(req, { ignoreSearch: true })) || (await cache.match(SCOPE + "index.html"));
96
+ if (hit) return coiFrom(hit);
97
+ return new Response("holo: offline, not cached", { status: 504, headers: COI });
98
+ }
99
+ try {
100
+ const resp = await netFetch(req);
101
+ if (resp.ok) cache.put(req, resp.clone());
102
+ return withCOI(resp);
103
+ } catch (e) {
104
+ const hit = (await cache.match(req, { ignoreSearch: true })) || (await cache.match(SCOPE + "index.html"));
105
+ if (hit) return coiFrom(hit);
106
+ throw e;
107
+ }
108
+ }
109
+
110
+ // ── message API (page ↔ SW) ────────────────────────────────────────────────────
111
+ self.addEventListener("message", (event) => {
112
+ const msg = event.data || {};
113
+ if (msg.type === "holo-warm") event.waitUntil(warm(event));
114
+ else if (msg.type === "holo-cache-status") event.waitUntil(reportStatus(event));
115
+ else if (msg.type === "holo-netblock") { NET_BLOCKED = !!msg.on; reply(event, { type: "holo-netblock", on: NET_BLOCKED }); }
116
+ });
117
+
118
+ // Pull the entire boot set into cache so the user can deliberately go offline.
119
+ async function warm(event) {
120
+ const cache = await caches.open(CACHE);
121
+ let done = 0, bytes = 0;
122
+ for (const rel of BOOT_SET) {
123
+ const u = SCOPE + rel;
124
+ try {
125
+ let hit = await cache.match(u, { ignoreSearch: true });
126
+ if (!hit) { const r = await fetch(u, { cache: "reload" }); if (r.ok) { await cache.put(u, r.clone()); hit = r; } }
127
+ if (hit) { bytes += (await hit.clone().arrayBuffer()).byteLength; done++; }
128
+ } catch (_) {}
129
+ reply(event, { type: "holo-warm-progress", done, total: BOOT_SET.length, bytes });
130
+ }
131
+ reply(event, { type: "holo-warm-done", done, total: BOOT_SET.length, bytes });
132
+ }
133
+
134
+ async function reportStatus(event) {
135
+ const cache = await caches.open(CACHE);
136
+ let cached = 0, bytes = 0;
137
+ for (const rel of BOOT_SET) {
138
+ const hit = await cache.match(SCOPE + rel, { ignoreSearch: true });
139
+ if (hit) { cached++; try { bytes += (await hit.clone().arrayBuffer()).byteLength; } catch (_) {} }
140
+ }
141
+ reply(event, { type: "holo-cache-status", cached, total: BOOT_SET.length, bytes, ready: cached >= BOOT_SET.length });
142
+ }
143
+
144
+ function reply(event, data) {
145
+ if (event.source) event.source.postMessage(data);
146
+ else if (event.ports && event.ports[0]) event.ports[0].postMessage(data);
147
+ }
b/ffab5e5a4c043c5a5e4d1391583eea58a46c6f0e4141a9575e1c407a4ed25bf1 ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "org.hologram.HoloLinux",
3
+ "name": "Holo Linux",
4
+ "type": [
5
+ "schema:SoftwareApplication",
6
+ "schema:WebApplication"
7
+ ],
8
+ "summary": "A real riscv64 Linux, verified and booted in a browser tab.",
9
+ "entry": "index.html",
10
+ "icon": "icon.svg",
11
+ "applicationCategory": "UtilitiesApplication",
12
+ "conforms": {
13
+ "specs": [
14
+ "web-platform"
15
+ ]
16
+ },
17
+ "capabilities": {
18
+ "storage": [
19
+ "org.hologram.HoloLinux"
20
+ ]
21
+ },
22
+ "comment_shared": "Holo Linux vendors xterm.js + addons under ./vendor and ships no OS-shell chrome, so the whole app lives in one service-worker scope and boots offline/standalone. Nothing is pulled from the shared OS surface at runtime.",
23
+ "shared": [],
24
+ "engines": [],
25
+ "description": [
26
+ {
27
+ "p": "A real Linux 6.6 kernel boots a Debian 13 (trixie) userland on the holospaces RISC-V core β€” entirely in the tab, no install, no server, no VM manager."
28
+ },
29
+ {
30
+ "p": "The kernel and rootfs are content-addressed. Before a single guest instruction runs, Holo Linux re-derives their SHA-256 ΞΊ and refuses to boot on any mismatch (Law L5). The kernel's ΞΊ is the same content address pinned canonically at /boot/kernel.uor.json."
31
+ },
32
+ {
33
+ "ul": [
34
+ "A real riscv64 Linux 6.6 kernel, not a scripted shell",
35
+ "A genuine Debian rootfs β€” bash, coreutils, cc, apt",
36
+ "ΞΊ-gated boot: bytes admitted by content address, not trust",
37
+ "A persistent, writable ext4 on a sparse ΞΊ-disk",
38
+ "Verify it yourself: uname, /proc, ps, compile + run C"
39
+ ]
40
+ }
41
+ ],
42
+ "categories": [
43
+ "System",
44
+ "Education",
45
+ "TerminalEmulator"
46
+ ],
47
+ "keywords": [
48
+ "linux",
49
+ "debian",
50
+ "riscv64",
51
+ "terminal",
52
+ "shell",
53
+ "bash",
54
+ "kappa",
55
+ "content-addressed"
56
+ ],
57
+ "developer": {
58
+ "id": "org.hologram",
59
+ "name": "Hologram Technologies"
60
+ },
61
+ "license": "GPL-3.0-or-later",
62
+ "homepage": "https://hologram.os/apps/holo-linux",
63
+ "releases": [
64
+ {
65
+ "version": "1.0",
66
+ "date": "2026-06-17",
67
+ "description": "ΞΊ-gated boot of a real riscv64 Linux 6.6 + Debian 13 userland, with an in-browser self-verification panel."
68
+ }
69
+ ]
70
+ }