Evict apps/holo-linux to kappa-store (M3): 27 blake3 objects
Browse files- .gitattributes +2 -0
- b/00b5326a4545594a940e078d74bec9064a36162a04eb3fd2a90fe1a4af14d841 +3 -0
- b/0be65ad91c9fc0c898229b532748d569154398412d8737ff5e9d13f5d6b92a99 +2 -0
- b/1fb7d44bf66d1ba1e77065e78cbe4666b95f7daa8e714117c98507072ddc1f4c +2 -0
- b/2b0ef5fab86c2f515e457cd0238e958f40021db7c7d84d3dad896cad7ca83ea7 +498 -0
- b/3cd10cb95409eb044bdc15bbfeae1d380d98e4cb6b5635e3d7e5979d30eba593 +17 -0
- b/456ac33710c2dee959da5703727d01fbf5350a51ac24077488ca1a9a0332e08f +2 -0
- b/481ecdef6a6186997d3771adbe807393ed61d8af62e4897f5cf9314abc447c50 +0 -0
- b/4c3cfb4598607c4de31a9a6bfe1b2de3ecac4a3cc9d347449913ae3c22299fe6 +26 -0
- b/69e9bfc55ac61137f83090028126b4484b19bd3075a74e9cd0c77aaf1e489bc3 +329 -0
- b/78381834c93186218a1c3be259d514cfbf151e40b99b781ea9feb79e371a99c5 +21 -0
- b/78ec7022aff858c52db5fda0a79a03b757d9d2b186e3c25ce3357ada0319985d +16 -0
- b/816d2f31ed1feb6e4d18e69c6bd81cdf9d1cc63fd650680b9f4ed83867cd52c6 +696 -0
- b/86444b599981098edbc377ed17cf81236efd56321edb86119dcf516f80fc2632 +3 -0
- b/95866a33a0ac86be3a1c8b454dd23b1c6279133ff195dd8d73c3ca98cea1a31c +8 -0
- b/95d704019ecda0ed07685277867299b1e7cc94c0f2e435c9f047bd5cdeb5cdab +79 -0
- b/996b3aa91392ca5e20a66924127be48eb5f804ad1ca48525f9363ff010307427 +26 -0
- b/a0cf7e001e6b509301f709a952fe2f5073b95ba618dd991874ef26aa85d8891d +33 -0
- b/ac97ac99cb90425d4fe91bcc20c7149e043a2574f62999c79d28ca8515229376 +0 -0
- b/b287e4c57be0b52c3182bf43edc16bda29a1dcdf0d5ec2d486e1944f8f13d654 +2 -0
- b/b2bb781c8c15f4717080e7bde1195d79eb1363dd734633c630f4c587670eb884 +27 -0
- b/c2fbf3e19ed81c6b5ad45c60b95289e5b483789861f7bd277ca99bd85897047f +218 -0
- b/c50bbb654dc8ebf24e9ef81a8d187a66de48a1cff620cf54fb2ff8f74832f799 +21 -0
- b/cbbef78890e83aa015dca82a2d6d170e5865de80c57fd45dcd48535e0d90d0de +2 -0
- b/cdd0afa46a7402609a1dd436aa41cafeb01615340e25423a2af8e9ddc7c4026b +62 -0
- b/dcbe5d1951a6b28a0e69ac955073ba55891275b754ddbb232cf12bfbf9bf1cde +5 -0
- b/dd131ed675da2a674042c42fc01402d48a323da675371368e89a4342cb02ccb1 +147 -0
- 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 & 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) => ({ "&": "&", "<": "<", ">": ">" }[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 |
+
}
|