-- CeetOS signed release. Generated; do not edit.
-- CEETOS_RELEASE_MANIFEST:e2NoYW5uZWw9InJlbGVhc2UiLHByb2ZpbGU9ImRlc2t0b3AiLHZlcnNpb249IjAuMTkuNTAiLHNpemU9NjgwNTI0LGRpZ2VzdD0iYTM4YzgyMjJlNWVhNDQxMzg5NmM0YWE1MDJmMWU0MjdiMjg5MDM3YzlhYTVlZDZjM2NmODg0YmVhYWI1NzM3ZSIsa2V5X2lkPSJkZXNrdG9wLXJlbGVhc2UtMC4xNy4xIixuZXh0X2tleV9pZD0iZGVza3RvcC1yZWxlYXNlLTAuMTkuNTAiLG5leHRfcHVibGljPSJ0SzM3WHRWQUdjalc0bEprOUdBMjN5UEsxQzlSUnZ2UmRySTJqaTJSSFBGbyt1Z3NtOVBhK1NibldxRjVudTVvQlN0aUxmVGFScTNMeURycHpNSzQ1VFIrNFIxclNqYmlPalVIc0VibzV4MzJJUzYxMEJvcE1FeWx4bkpiSThPdjJPb05sRkJiU280d0N5a1BnMFFkMFBISW5hVURtWXZOYW41VlZNQXVybkcxbVhBUDJJMHFJaXpQbHVtTHlkR0JNUmVGTldUcGRud3BwQW5KNlNWQ1VIMTBlK3JOMFJjaGJZMStaaGN2R1VnOXlOMUJkcDVmVS9WSSt6VDJFOHU1aVkyeXhBODNUMVlHbDRWaWlKbXlnYTY2bkxoTGxKcFo1cEE3cDk3MWxWY20yRjFOQ1hTc2JiczYvUm9Dci8rdTEwdHA0dkQvaTg4T09ucHdMd2ZHNWppTEJLSWVTUXVyaGcxdGJlM0wyNGhESEozS2lMaXhoS3N0NUg1L1cxWHdFd0c1Tk5zbGF4cnU5UlN3YU41REcwbTNGQWM0Vy80Uy91dEwrQ0k1Zko1NzQ1VDhieHFBNjRwdTZrUEZlUVBOdWJzeEdPSWhNOWxnN3RCNXU2a0VHT3o5akM4d1licjkrc3IxVzBGU21sL0JPOW9VM0pXN0FDZTBKNjJDbEJtVFE1emkrSTQ2M3hvajUzUlRubVRTZ3pGanJzT1lncnFMNWlYWVRBblVremhJVkJJNDFibHZiclAwcjhTa1l0Q3VSTW1pa0JsVzJ4MysySUs2K053SHluQXVwYXlXbWhDbFRPeGpUSVBvQUpSeWxTVTYvUXdrS1Z0aHlqSmorR1ZzOWJSQThqUUc3Q2dYNlQ3YjBHelh1UEhNQk51bS9TN1FXMVdtVXNlR0h2VlhWWFIvTUFaRjhSUUg0aThXSDl6VmxoS1d6b0pyUEk2TDBRRkpTM2tES2xmdkRLOW1FYUR3eVBQd2xSbk5wbHhYZElXOVljeTN2RWxKNkp0aG5PVjRzL21CMGVSMW01U3U5bGpGdVFidjc4bFJ6RXpocFJtSGxHRWxzSllGWDQwaWNJUUV3NTh0YWZ3Q2plM0N0ZExHSG9aM0tDNDRJOUFyWEJDVmc3M2tDU3V0aWg0cFhxQlB2Y3JCRlQvcEtoQms4eURtbUN3Q0Q0eFBLaW01S0ExVmxPSWFESUpmUENmVkVydG1XK0JiakNGMERtZjYwWE5QNEd6d2J4T3lKU1JvSlZKaGEzTnF5NTFvNUZINEl0WFlQeTRYSnBORFJXRTdadUZBZEdSWXlnOHBFZzRTZkFDSkJRUVVhUEFEejZVeHJGSkRFSW04WlFEbFhJQktxRGFKMFpIb1E5VC9XSmh2WXNoeTh5UGowSDROUCt6ZUkyY0dvUU1xbms0eGRIaWFtZXdKdWM1QTBVNTRscXZKY0QvZUtkTjJhMDNGeGlZaHFuakNJWXovSW5nWmZIaFhDSHprZXRJQ25zZFR2a2xHVEtPYVVkRURhWGNsekNoS3RpNVdsYVpEV290QzB0Q3VNL0ZQY2R1bGVEZU1aUzBvcnUxMk0vUmFOb29MQlN0NlM4TElRcHpIVDB3eVdkMldQTE1GUDhhMlNYamV6NjdtY3lXemlzSytoanBVanZNUVVmcWc3emtsZ1d0cW1hZExLc0ZvUXFXQkFpZFFwMGIxenBZUi9PNVNZazR0bHdyRDFmR0RldlFuUFk5MDBnTThGanByQ3JMQzNqVXoraHRYWTlOSENveE9ILzdPaVJwNHlkTnpwNGF4Y09vbWx5bzRQMFRNd0ViampPaGFqQkRVZmpDbERObXBIMDlKb2pDZVJzekx0QWVzd1JGU2cwRTdEZkFUa0Q0RTR4bzFnZHlpRlVVTHRiTUZCdjZQaEFJZUd5NFdWbnJnN28wOVZ1Q0pyWkhMYmdqOTNhTi8wdTlaUnN3YThWcTdyR3RCNkNkMm1FeVcwUjNtZTlMMlgyUzI5UVNmTHFKK2phVWJKcVc1UGZZZk42R1RQUDFtNnk3MldHdzFLQmRtNjdSNmZqUTdkRUhOQVQ4RUdXY01CblAwOFBKelc1N2NpaGtyZ2I0Zm8wNFNvVGIyczl6Um5JR3BLVDh4dklaRDhZZ01pdVNaWExXMFZpWTBuT2RmYW8zdFpVOHhnT29JTm56WC9qTmhmZG5SWlVoY0gvbHhQbHNxUlEzeTVyQXZ5U0E5Q044MG9WUWluQ0t2N2hNSHdpVmJnUDNxc3AvRTBLSmJxZXdJZXNMSi9ockdCUXN3RzRta3RwbG5HZUtSSzhCSDhmYkZKeSt5WjVjdndkZUVRYVMyWXdKTDVHNFF3QlVWRDRzUnFLT0N6WGZUODRSbVBPaWJ4VjFWRE8rT3VCUzVPbWFTL2sydldtV2pyVVFCanFZTmZLWWtQaGR3WXJYNGkzZnREWEhqdHo4MzFreTN2RXRwZ0IxRnMvMGVFanlkK2RCdEM4WVhJU1A2OFBXenRxK0t2WEhmcWY1Mkg2ZlpRMlpDcjMvK2VvRGoyNWhEMnFDL1ROZ3duQndhQm9JanlKdTZPUEwrTml1b3RVbHREMDZTcEQxaU04SHFVZENoRmg2SlpLM091Q29zeC94RCs3SURVTUJTYzBmZ2pQWURxdlZsR1F0eTlBckNWWkJXRjdxNTJnVFZSaUEvVmEwZlY5NmpmU2VJZGRaVnZLaklvU3NicWJzMWFaeTlBTUdMZks4TnpSenkxZUZYUm9QZG9mVTgwTnBzNnJpbjFtdTloYkZOSnVRRTl4SkhHci80dlNPaFZpQnFZS0tOR3hGRjU0aWRlbk9LaHNxSzgzTFhlYk02R3BxRTQydE5jaklSZWVhODVqMjduUnpQbGxaaVM2Y0Fnc2J1OUlqQkpyUDJleEErQVRUM09xemIvOWM2c0pQNFRWcVgvMlcwbmdGSkVCYW03dTU5VThSNThtdUhrUjRXdzVXWE0wSHJzSUhKekNNakdNMVNwclB1dFY5djErTUVwWU81WXNpcXFMdW1tQzB2SGo3RUt5dU50dzFXblpkUVFVc2NHbkRma0UzNjdQd21OSUJva3o1NzVoT2h6L0ZVN1JiRjQ2R2o3THZJMzZ5eUg2Tm91dmtwZGpkNk40aUlMSUNQNyszZlhlQmxLaEx2aVA0dHNvdWs4VFZaTmlOZEtWcXhnbG95T1hNdmVXMHFmMjR1bThmT2dENERoOXFqTkFGOGwrSHNsZkRGa0ZDNDFGWjUvVXlKNmV3eDNlTExXbWd1SHU2Nmhuckw3RFp0TkRqR2s2bzBLMjFQakFWL2grV3FzeVBCcnJnNFhMQzZ0N1BPTUNhYW1XbGh5V0Fvb0dQeEM3UUxvcTJkNTIxMEw3Z2hwaFg0aUpPMnozdnQ0VDFDSDhtdDMxU1E3ODFHM1JZN0ppaWJPZXpmaFVCd2J6YUk2cGZvMWI2ZEZUcEdFRENrTzFkRjlDRXJoWkRYY2ZSY0RPWlYreTVudGtZTkplZFFndHduenNEQmJmUWJpOFdpTG5wSEwzYW9haWlWRHZSL090RGR1aEtjc3BKVmRpb3FUQU51MjF1K1hmMzBpVmJKUDV5ejIyZEs3ZTdFaVZrYWVPMG8zOGpFV1pzNUVpQUlac08ybnF4NkxOZG16Sk1NOWwwR0RCVk9SdHovRkpuek1RL3ZJcWtlUG1rT2tKQ0xpcElEdWJMNmx3ZTVjTHRWVHNSL2tNYVV2cW1qbERadWNBMXRmbWFBV1Q0cmNLaDlDNmNlV25nWHpQdVlwek41WXN0WnNCeVRWRjkwM2hQZmlndlBnSUpJVnA3aVdQOFN1Q1pwVGE0MzlWN2hwa1poQUQwaEJxdHl2YWlzcU5HTXQ0VVpZb2RKQlFBWUNrRklueDFpaEVXSkVBenJ5MDJKOVg2V3F4NDQ0VGJnanRONEE0VGFYcEFSV3EzeTlMWitUeVMwZGoyb2hmbXJ1dTNQU1p4dFRIZnRZUTh4YXdTU2FDaTFJbndtQzlEYU1ZeEFhRFRtSVJPWkh0UkdsdTR6amRhVGlUelhWdUN6QkRVTG9YcXRrTWFQb3dReElHRzh5dGt5Mk8vQzVnMHU3Y1ZqVUgxdlA1dzA4UENhMnRKM25sb1kwTnNuMXlEb01pRGpta0FDMnhSaHVPMjhCT1A3R041Nk0wQll2dFREUzZ1NUhkR2pFQ0pIT1krbU04eWdlTlluUWRGYWlBMDdJZFQ5RWJiZ0FWclpPUjJ3TngrMCtpZkJWR0l2ZnNhdElPYkEyaHEveGRFaW1SK0NJcDlObm0vaTJCM1Arc3YrcUlpYllyamtYZHQrcjREcEZEM3laQU5TL3R6K2ZiYVlkT2t3WEw2amVQRnkxc2FnK3BQTXFJUllGcU1abjVnenFVb1A4azNqNHU3ZkFCVFFQSDNSZnZwbzZ3UFJNQ09EQXRMeWF2YkR6NG9xZ2N5OWxTdllvb3F6K25HWEF5RCsvd3dzMHdLeDFXaDlkM1NUR29zSjJnNmp5Vk15ejI3WTd1a3VBVlN3TlNOSDJwTmg3bFduSFhDcUx3Yk1ndDhKOElLL1VPcXJTMDM2RVJDaE13NVl1ZlFHaGVLdHg1MEpiY0NRK1FiNVozT21aM3hEV1F6OVlPUE9yaGNtbitxTTBMY1ZiakNYZ0x4UGltUHJhSFNHSGF6QW1tc1hUUzRobEFsTkg0d1p0ZFB3c2M4SGUwZVk3ZVYrN1FocnpHTmNPQk40czR3WWlxeVowTldpSWJ5NWJ4akhNdXVmNUdCUVlrbnNaNFR5MVlnbDk5N3g2YjY5Z2g5dGc4OEhoQllvRWh1YWdJSC9TWkNPcngxVDVCd2Rocm4vTWdGRFJLVFNCQ2hjeTc5Rkp4WVpLNVI1ZzhmK3cxejFMVEJZQ201azdTWEhOTVVvVTl1RHJMendYbUwvNnBTL2k4c1lCdXpiY0lRVkZOUmMxTFBNbXpURzZQMTV0YXVCQ2hSK3NXU3d4bHE1a1Y4dXZ0SlhueWNZam9hYnlzcCs2Y08xc2RpNzZ4MlZjVHpnRmhzc0FqNWE0dDJoQkNTb3p4d3ZwcFZnUjkvTnFCZlVIdGdwVUxLbE9nZDJDRXZuUzYrK1A0ZFFmd1pwZmJtTUxOOW1OWHdZNFJrcjVVYkl2NzN1bUduai9IbFBRczRZMnJXZG5pYUhmd3RBMXlBL2E1Z3cwUU13dDhtanU4OGd1UFY2c2VGUHBvd0FnVFlCaEg4Sk9TUEIvRFlMWi9WLytqeTg5OVVENXBCUEtVbDRrV0ZnLzdSRVRzaTc1N3RIYnFBREN0d3YybXdVWUo3MWYvUlA1UjZWQldEQkJxOUJxaTg3bnIyVEQrZmRyVkpkTi9FQWtqMmZyUG1qeHlJaUMwYmovN29jWXA2REJWbW8zMERrcnBzTTdsRy9LVlAwOG1IaU5yV0RuYkI2c1ZLUnhWOGVpUXduOFRGVlNGZ0FVVDN3eXg5WHh6c2Q0WnRMazNrd25ocFRDdzZjMUU2THVsSVNXQ3hKR0hXQWI4OURzQ1ZnR3FDSkVTU0hoRlNKdTQ1TEpSTENHY215Z2lLcFpVaG10K2htNEtia0R0aXdmcDlGUVlzL0F1Si80MmFWWG5IZzQvdGFxWnZXNU1lMHBQTGRPdWRnTDJIRXJDRFlwWGxZbUN0VWVFVTYvWlpPMUQyNlhBOGY2Q2JGeUxnbkR3MllZUFNNbmI5NnExNFBRaFpMK1RLaHlweVQ1ZlJMazFmWTFJOU9kWnZVQVRLQVdoKzVkZFplaG5kT1gySllyOGpQcmdxNkxTUk0vNTFhbjA3Qm5RRkYzUEkyK3lRNnpvUmwrbEtlT01vMmNEZDNlc1Fza1RWMGZGOUw3YlNweFErbG5TVTJGNlAyUy95aDU1azlkRFdRZWVmQ1NzZFB5RW00TW5uNldVQ1dJb3BLRUpEc2lKdGh4WlA3VzdPcTBicHpkRFlpZGVVSDVkWGF5QWZLbGxMRU5nbHB3SjVEUm41N0pnemhCN2lRVFhDZW5xQ0wzTUkyb040c05iL1A5eiszVGxQSEZjRUJvNkZuTFYzUlo5T2ZrYTB0VTFKdm4zUDRrM1hsdzl2c2dxSFdYdkdzR0F1NVBiTTJ0cVc4OFZqV09lRzJHNjZXaERXZC9sVjR4OGZsZVVtOW9ZUHYxenZzcGVOSnpnZGpxdXdxcS9EejdvR0VhdWp4cHRZYkE3ZGh6NUFoUTVQcFZvZkc1aVhWbERnR1JqWXM4ejQzU1pxTjN1aEZFREFCcXNHejNCaGR2SUMvd0JqRXp5VkpGcXRaeFBZbHpPNzA3SzZaalQxNElqTURtd0hGRGdZdmtZT1lHZ2tpQjdRWk02K0JiYVM3T1NwQmk3MDIwMlk0NG5xSkxsR2EvYkVNQWxKbFdvTkZESDFMRmtIdzc3RkJrT1NkWGdmMC9OZ2FHN2dZbGszYkpKeEd3NGpscGlqdWVuNUNwS2ZLd0VhTUZyM3JyU1RMKzJ3cUpSbUxDZ1NXZGxXbmU1cFhUNXRGb1A2dW9GT2xNVUhXcU96alVoSko4UVBMSzk2eTVnZlBORC81bWg2eU1oSlBQZXE0MUEyYzdmemEyYjh0bkljdXhwajJXL284QjZkbjZiZWR2N2ZWTjFIdEtVUm0yUWNKbVQyUTNQREt3anV5K0JIMTlnS0NRb1BtUndPbmdQSmhXcmxsYk91bHJIN3U2Y25ZRzZJc2QvSUNaY0h1bGxMUXl2TDJabXlEdGlVckE1NHc3VkR4VDJRNXlZYU15SFg5bTZZaDJGanBrTXpGSnhiclBMUzl0OW1IbENWUys1a1kwWERuemMvSlV0RUMzL0Y5QjhTaE40THFubi81TmxNQVRucmxHUy9Hbk53TUI4OXgvZ0M1aTF2eVJPMW8wS3puWXhvR3ZlSkdOTUtTeE5ieEkwaU1BbTlrTHB4Y21UdEluYXlWdlpRaUMyeC9xYTdoOUJBNFhtcm92NWNlNGRoVzdGQkZHNTdtaUVlbWpBSWRHTU53UzdyR3JSV3I5SWtZcVhUdDhub1ptL2RxSjZ6a0FVWWFHWEI2OW9lbFQ3VXFuOVNiTEFEa29SVjRCVjZ6R05EZVFWQ2pUOWpVdXcwb0lNb3YzaUt3ZzBJalFmdlJrTHVXNUV0RDUySHBiMU9Qd3FnR0pXK2ZqSTRtOVh6UVFubzFhY0VMS0QxcHV3cDEvdkUxKzN2TGxSOEo4eHFDQnJvbXdHeFdNNXJaRjJWZDYrenpSMzNTTU5qQzFtaHFLT2FhL3NLS2tFN1ErSDlBUkQycVlQdWNrWFlpK2JUWlJJeklxMjI0cjBlUmQ0TUpqY3ZHaGZ0MXVMYWQ4dXVFeUxXQUU3Qkt2REZHSDFTd3dnU0JUWGdFZDhwaXFZSFNJbjBKSTExODRKSVV2UlhocjhldFFHSlJsRVV5Z1g0aGxkU3R6RldwT01BLzdFTUErVlBCV25RREUxWXgzUmoxWmllbTMzWVk2U0V5RzQwTUVQdVNSREhZOHVBNC8yZWQyWmdpZmplZjRwdlcvTklacUJmNXpQY213V29EeHdGdHFOekZFUVlkeDZNdWtoU2txTG90ZTN2WDlQcUtkVDFocnl0RDRrc3I4NGw5S05HNW8yczNia0pqZlVzWGF0dVFQRDZ2eURuMXY4bWw3S3IvTzA2NW1pVzdHT2RQTVFla0c1U094cU9palNkTmpSRlF5VUlkYnY0ZExyMGVzRzcrNUZrZVRXZjQ2bnhMcFRTQkNpTitOZGlmLzZ6RVp0Sjc1dlRXVWhxY2diaHFOdytFTWlzRkNOK3ZtWFNydi9nN0wzcEtHRk5RVlZpOFIrWUozUHVac3libjBaRXJRUDVtQXJuVG4yNXhqaWFwb2I3dGpKdE9hNDJXTkF5RWswWkdwcVFaM3JkdVlUS2VhK1p3cW56VmZGbndXcThVeVcxNS9Yd0ZuLzlrOGJyL3BoQ3dmajU3ZDZSdG1kS1VMOFRjNnNJYmpDZW9nSC9ncDN5c3pFZTNDdkhEZzhTSmd3dlkybmZZYnNBL3ZBWDQvWGlSSnFpQmx1MEdkV25jUy9GK0VkYjhqRmFVNzQzcjFRUGZ1emxVRlRJc3RxdWszSi9TTkVrMEdubkpsUTFJMStqMkUvNk1zUmlFVm1hbWRvVXNYNVBlaWo0Qm4raEs2UTZFeFBaOTVWWWQzYmhVREF4Kzc4ZWwxNEVuVTY5bXRhK1IraHFVTnZqcDVBdXJYa1Btd0QwSTZKQUtWM0E2THZ2YWZzRUR0ekU3dnBqQWN5U08wMHhyVXJhVmUvbHREd1hDcExYZ2MvMzJuK2hQME5SN0JTVlRKQzIyc0ZIVHVsaHp1dEtjaloxdDhNdjhhVXlRMG9kSHhvT3lpMGtPMGd0ekJyc0lKbmlkNENqSTF5UW9JTitERnZleGR0M3Q0SWlkTU8zYlpGbEZHYUpJMzNUcEkvRjF4S0ZYTXM3YnhuS0ZTdDZmV0NPTDA4WTBDWjVpSG9aRGM1VDF3ek1NNlk1ZHFmelhNUG5pSnBHczBweW9ramNkM2xTTFM1SW1qbWZXSnJFa1NNVmYzQndNckNOMGFRb1paanhqclFrcWhvWlZFdjIyZVRFNEIvK3dWUHk2Qk8vR0ZQZjQ3ekE3WElsUUdET2paT01oSndWQlQzOWcxTmliYXRadWVzNDBnUHVCYnczaloxUzdVL2VDQkwva3c4b0NJUXB6bU9mWFd3YzlnVlgzRVVjOXFyd3V0RW90ZzFhMjQ2TkRtRStzMHRrZGRvZkFBTEx1ZS9yaWZwaFlVNE5oMHZNRDFka1k0cDg5RmRkMStuSDJUc3Z5UjFUditiRG0wVlQ0UkRITTFPRDZvN0VpaEgxaHc5Sm05QmhwZE4rczMyWEpwQ2FYZldicGxoazY4cC8wZ0twSGx3aWNzSVFVd0E2Ynhsd0hybFRkL0dKeTNBTkttcjBsY1p4NjB6bFRqdDQvNHFYZWtFSUdnckc1Ti9PdmVrR2RjSVROWnBROHNNOHc0Vml0bmxaa2pYV1JZcHBveURGQnhOVlJFblRDMWhDbkNJWUtubWJWWnNrdUMvVEtqclo0RFE3Ri95WTRjTkxScFUwckYrQVNsSXZsQkR0NjZGam1tWDBkVXBFUjZHSk8xUWR0dXV2cDJDZ3ZZMUpUcmJ5Y1RBU093ODhlQ2dnOXdpT0pSZnBuZ2hFVXd3dHY4QzJLdUxUd3dOLytkc3N2VHJ2Q3BTbUhBL0hWQUZvUk41Z21ZaU5LMGJvOXdJRTZSa254ZTlEajgwL21lMUJIZkVHRmdncUxoNEJjZkEyOFZHWFc5ZytXQUhwUTZLNDNidW1GTUFIbVNhZWM2ZnZTMTZ6dGFwNS9BVDNzamZaYlFCTlovcjlQRWpTUkZSVE14RThzcnI1ZUxDSTZSTFZLQzZaWE9mSExoN3IrOFh6UmdFL3JmdVJ1RGczcHNkYWpXOCtGbDdtYzFBTXdvbDlHL3RlcG5CRUxTMEt6azdKUXUxNWRLc3hpM3RRVzB6VE1scUFiTnY5S3dFT0VKZWtacCt6QTFwVWxhdzFzOWdQV0JLdmtrVWU5YzF0a214VzdxMGx5SDlkdnJJZVBwNEFxcG1tQ3lpTGFEckp4VXVUTE8xakpuQjdxZmg5T0l6KzJOV2FrWm5Jb2FwWnJOandPUGdSMlhXaWVuamF4VURHQlJVQXFZMlUxL2dWcnlCL040UERCV2hyY0JvMU1GNTlleU8zdTk3S1o5bEp4ZFBZTkJST0ZYeHdOT3pDR1V2bDJqOWhVVjh4MEx5cmJsSHE3azlrdlNXeTJUbmJYQTBhLzVMNFdnaEdOWVJPWENBV1gvazNKVGh2dWV3ZERPa3gzYmRvU1luSld1dVlVbmZ5d2ZVbjVEamFLWWIzSXNFbFNWeTZ6TW5wMUhyRENzSXZjZElYKzF0TGZLUTRnL2pRVFFuTC9uekJhUjJGTmRxcHRwakR3cVlVSytPOTcvdG91QmJrY1pIMlBVVGUxQlpLdldnTGtpbWJJVFRTMkJuUm1hS1RRNWx4WllLTzErUklSMzhYQzhYa0J0UVRmVHNiMk5zVVZCbHB4bXVFdHZYdFd3NnVud3hDTitiOURTWWovRnRhaXZpMnZVVVBlajZwMjA4L0FibXo4R3dBVGtVaHlrZ0JFdVl1cnFqckE0VjMraHVUYUJ0eDcwV2ppemRVcUQ1NXlkOVVodG9iclNVWTVETjFXNEE4UUpMVlhQL1NZRmlRTFA5VEs3dGJQbXpZVlRrTmJVamVEaFpyMm5NQ0ZqT0lOZmdrYnZBUGd2c2hXVWhQaGlpRUFUak9rUXR0N0htSFYxcmt5b0U2Q2ZuV0x6Nmt3VmxTZTZnampGZFd0WTFzR0oyeEYvcnJpQVlBc2tMYVZIZDlPL1pBSlhqeUpPY2M5NHB4WkFpclRmQ1VQa3dIa0EreFlCRnVibktMZ0hXaC8zT1lsRFl6M1htNlZYVHdpS3c2Wm5UT2REV0RKREFSaEZUckdEL2RHdDJOU0E3MGNQVzVMK1QzeXhkZVlaSEFTYS9QYnNEQndVeFZIRzhCVzUzQTJQQ1o3dkl5QjVKRjhZQndFbnRNcGxBQ0pqTnFITHR4TlpaQ3ozQmRvMTVmUGNGcVJvcjh2cEJKd01sYWNqUXA5bzFoOXZRYWsyL3JkYlVqeTkzeDJoMmIrZnhrT1htREVGNm53bGhSZy9mdmR1a3FZMTlGSVFFeGFtL1BaTTNyNzJhREZ5ckZZZ1BvR3N0a0dkNENlUzRRUUZsVUEvVkJGaXFMcnF6bWFTYml4YVRmSnUrcW03QUFXQVBlQ0VOK2xTT0xVZ3RVQ29KaTBTd0kzY0YrVEtObWxzZzZ4bk1WZDI5dmhldzJDUVY4TERpeVhJdmM3aEgxRWFsaVlaSFdrOWpoTFJLQkpISTlidUliVjdWbTdlYUJHbmlrWnovZXQ1UEkwanYxRDlKOE9ucUkvYkFMa1lIdi9SRkZaRlhLNzJLdnBLd1pFL3prR001cE1aZDVUSEpPK1FQOVJsak5MY21jc3FOSXA1Mm5BUTV5dUQ4VllHYzlZcU13SjRKT01TdDBLd1AwcnBIaFkrblk4cEZQSmNML0ZNOW5CVDNlbXNoZ2xZd25xNGpUUW9NK1BkZFJXc3Z4VWVBa3QvdzAxd1lyejhJK3FLbGlTUGN1K3kweW1tdFNrYlhMTWZhNk8ydW80K2w2UUI4blg2ZnZSeEUxdkh3ZjkzdWVZdU1KVmJGSHF0U3hCWWhScDduTkoxMWEwUG5QRUk1LzMrb3ErRXgzWFUzT2paQ3ZQeFkzWXc4amtwazZwdGtIL1FqTTVwbUNlZlhpVjNSVXdpS1ZkTzdlaUN2Q3ZhOVIrRFE0MlJqN3FoSzVkcVI4TGpDR0VxRUFlNk1TVS9GK1VGNnI5Q0xraDczOTZTQmJpTEZRTWEzM2g1K0VDbUZJYmRFOWU4UWREVW1YWUxaOEp6SGVkMHVrMjBoa1hHMUcvTzFmWjlvVzZmT21KVkJlSVR4OVFTUFVWNTdwMUlkUHRBNDdlV2ZkUXZRZzZjOG50S2tXZE9ZS3I0SWt0RDRVOTl4WVZhTGl3UXRMUlg1QWFUalRsWVlpQ0VIT21qNVB6aU4yZUdLS25mZWpjK2cycXozaitZWTFyZzRmU0RDVlYwaVNTcEl3dVh0LyszK3pJeks3bzdmdzJUSzVoQ2JXRE02bklmQ09lbWlqU2hjQnZHTWZQTVltU2U0SCtqS21IWEQzY3M2R0ZvNklkbTNkMW9GRkJsci9ERE9UWU1XWjFxeStaR2VvM2lSUlpBNE9DSndld1VWSzgvWkFPb0l4dFVZMWZOOVBJWlg5bXAyemhtbUlEV1FyYUh0Ny8zNFBpWUxRb2x2Z1VscldvN0NmVE1Oa0RZTVhMZ2VveXpNSHZzL0JqaUMycklzVnZoS1JCRFkwSWd0dFkyUEF2d2hpeEJRMjhocWpUclNpQ1MxdkoxZUp4RGRsM2lqeG1neHFxQVNxU2d3bmhZSUwvMEczSHd1cFJ4aFBWWUxVaHl4ZWdtbzc5eDJtdklPRXpTQWFQQlJRTVIxUzBJbVYzSW0yZzR2dkFQWGhsWk1FRXJ6N21kbHMwamR2emtOZ2NVZjJkcGQwR0JWTHJ1MVBzQ3lBeWVhZnJwTy9RbWNOY0hCVWVNSzAzaGd4WXlSZWxYR1l1ckI0YzIwTTQ5N3Y2ZWZyVC83ZUtHU0ZaUU5pQjMzS3hMWHBTVHNScWZKWFJVOXd4MEN6K0xMdnJxaWROeXFWbzJKSjFINm12MkdnelVuSzY1bnNwK29wdUVSMEh5U1g3Ymh0MnVOY0hLbkk1bmxvUUpHam9sUWUwSFRVY1pPUlFjVlNFbzNsam5LVUhwZ3M4MWxKY3NIMitQdUhEVlZIR0xodUVhWXpjbTF1dGVJbGlPcVlkVXBNWjJlWHJFQ045QnpvYWsyelJDdFBsSHR6bmlEWnpudndVd25OVmlKNmtGZlJxd0NPR0t2dUtXS0pIa1A5U0V3Z25TblBVb3pyYzhCSlg4TTRwelB4azdpRmF2bGlrOFc1MFcwK1pNdSt4TmRVbVM0eEhKNkR4WkhHQUEreHVTUVhxT0hJNG5FRnhnc1IwSVUySmNLT1BYVXh5bUVaTW9KUVVJUnMvUlI5ODZ3MU4zemk5c29OVmZ1ZDhJR1pDOFc0TzJ0ZlFVNC9mRkI4RFNlWkdFY045NlRzR0VnNXFHMkFYL1RHSlJuS29uVUFMV2RMdmtzOHd4bGlUcUlCQUZWQVExM2x4TEkvVnlmUVM4TVlKMkFuekt0a2NhdWNUWVl1UmZuZlZZUVJFQXV0andKbFFOMkJPMm5wbzhhREdGb0dmT3QvZjhoazMrL0NaRVlwc1E5dy8xcEJONXF5RjkxRjUwWWphbjBaWk14cjR6U1FYb2hJTXVNZTNqWm02YmNMMGFUUnZmcFBXaEFzTTRITXF5b3hrMTNMZWhmMDJ2R25MM0dhUnRGS3phdUNRdXI2QkEycXRSTS9UT3lZOUVaQkxKZFpDUTl2TmFWYTcxcklFdHdQa2x4WW1DNHBid1gzbFUxWTB2UkdHNWlkc2RhVGdMY1ZHSVRDMUl1Tzk5d0R3M052blljSStDVjI1QmEzdTVwdkpENXF4OTYvazdVL2E5QXIwa2tzVitvV0RwNGFTd0VVTDBzVEtxYmRyL0RnaVdZNXNZMDZiUG96Q1NjL2V3TzlIeUFEN0J1VFhmRSszTmxrRjFDV1ZLK3FIRDd3WE1JTzQrS1J4T1JwcXhrVmZnbHhydDZHVjFLVFZJOVB5OUNYbU05alUwWUhrdy9yeHVodDRUaHhpLzRucjByMnJsdXYvL1l5VEN0S1QvcG5xMStQUGNsQkxWWkJ0bmJDS2pwSCtOYXptbVZyaWorRnRJSGtlME5LdmJkMUlhTVE3cklCV3JjbldOL2J3Z3NvcFJEOXUyNkF5dkNvcG5YVkxsb09WRnVpUTVVREpIWVJHSlVpOUZQOHFaSlhsYkFJRDFHbWxmTy9PNSs5SU81bzVsSlZtZG5OM3pjcmhHd0tZZmx3M3htREtmWEJKR3p4ZUtmdmJCZjhSc21CMXp4WmZjc1RhZGpqcnk5ZnU4ZXBaaUt4QmdoVmtqL2VLQ0NMcjczTit1aFdrZkpPV2hZOHphYTdrRU1RQ3M5TU5FUlhlRUhRK3JYZUk4Wm42NTF6Ymp2ZStQTE9wS3Zpbm85cVRMWEZHeERoQ01lbTJ3cWFQYmc3dWk2MHZQUDhKZ2MvaWY4bzhsY3N1RWRvZUhITDkxN1JaMWtRYVJxaUwybUdhdUtQWUk2dWxHZVJqa20xdnpjaDRsZGR4S0VDNHJ5UVlQN0lKVGpkc1pjMWxrb2ZaQUpPRVVWZ1QvYWNzU0JxZkh5R3F1U3Vqem5ETUgvd3BlblE0YzF6cGlHb1lCeUM5elFkdUd6Y1JQcWVXTzY2UWdSbG9vdkx5aWtTa0srOVpjOGFRcnFoQkpndVJqQW9mamVSMGx5eW1mcTZKcFRjamhrcDUrUjl6bUduQ1FsczlKM1oyeHlDU3lYREVlbThBVS95MG9oNVIxa2w4OElvNmJVR3RpbmprZXM4Rk1uSHRhMndIbmljTjFYZlFpUFZvNnJBamVkOXlKajF2SGlkVXZXZjVCVGoyc2JBaEFwY1B6WUJxOVlNK2Q2anI5K2cycE1BYjdIK0R4T2tEUXVBZmhyNkMyWGF6R1VyQWw5UGI2MjRtMlE4WlpXOVBBY2RoWUdOMkxpekJlT3FHNmZMTk1HUHhITyt3Nk1GaWlWdzlnM1JNMk5yd1RhdGJkU2s5aitGRGVBWGszNVNIaUVmcktqbkthSGVETGh1Y0trNTZ2RFNpYkU5ZXVVWXRqcWtOci9tZXR4WFBJNmlmcWRZUkNLT0JHdTBaWGtYaUNxbWNIbGRQaVE4dkU1aHdOZWRSWjJLVFVwaEFjTThSR21TU013TlU1ZThDOWVMaHlqYzNNMkR2U2pic1BBYWFmak12TS9PNExacXRLdCtDcmViKzBHaGJ2ZUYraUU3Q3VQbndqdjZGakVzc3RvanJpT3M4Y0dYV1Z5bkZLdEEvZmN1Uk5PQmVrTThMbUsxeDFSMkE0RldSL2VlNGc5Qi9hVDlvU0dvNzZydmV5UkF6VlBPd3VRc0dhekV1dVlaYUNyYU9BcnZxRGxUdCtXVXJ6eW0wbGh3Yk1WM3N2R2pQTHdSckRTNnQ2c2ZEbEI5TDdrdFdjdlUvMmQ3ODViZnlFbXREcHlYQW5FekxrK1VReWRIcW9LYjNHeFpHMGUrNHR6bk5waGNLbDR6ZHRyV21FbVJFREw0YzMrQzIzSEhsN0pVRitQWkZPMlFEODZaMzVpVVlxTktXT0RONEJua0xEakUxVWxZZHcxd0tsbFEvQ0Q3VkROdUc0QnlTK3Q4V1c5K0NOQlI2SFNYVmxEQmxHN2FtbGZzUTNUZUZBa0V3N25PbFVCaGRYeFdIWGJIVytNQkZOazc3ZzBXdVgwMmpsUGZHdXpmaU9DN21IYndDR0hIN0xYYVAvdlNmQmExZmc5RERvNFVFSmFuRENySWpTSXl4NlRKKzRyeVRkRDRvWjZyVkVSbjZjbiszTUV2OUdXN290T3o5NDhTZEZsMFQxSmZndW9ITUpTTEdSYVJRSnUzMDlnT1pFUGVWTTNadlNWKytSaUF4UFNrcWF3azdzdEhBV1ZUbytSRkFhR0hGVXE2UW4zcDdCK1MvVjdsWkw5U2pOSkdrU0czbFlXcS9GT1lONDh5Y0lWdGxtZ1J0N0xhaEFWWExCVDFHemhGQUF1K2JoRzRXQW4zNUhkMGR4RXpsYkJ6L28vb3JTSDJCWUdiOS9qaGNYQVFicTlJNzBEMDUzTUJFUjFoK0h4UHhXN0NTeUZrR1BJbE5WaFhmbHk5Z0J3T2VHaXNNdi9zRkI1U0d5aTg1NVJoUitMUHJjU0d4Q2dBQWNWTG56Wi9CSDRXME4yQnRtYkhnVHhyZHNoNFRXMmYyL3hVVnk1WjBYakg3cDZISVdCaG5QN2RaeUd3T2U5Yzg3NUdobzJiNm02Tk95ZlhXeWl6aWNTMnBJZllqNTBWQ2JTUUwrNit3ZWt1SHYzdENjVGNORFlkUnZDR1lhL3Mwanp6SlE5WmZDQ1FadFIwMUdJS1ZaM2x5OEhOOS80RHJibERhUFliZWhpUmZta2tCSmR3S1RYbzN1UGhpajZqRHVnVWFrWVQ0V3pkVjR1clU1bUZWZFEvemY2Sk1iWiszOWU3MUZFSFF6akhSV29SU3lXOVlST1V4V1RSMndac2hsUnhOczR3TVpCZW0vWTRSWW5nbGlucXVQejdkMHJIQWxPTGV1bElOUWo5YnloS0k1aGdNdmhRS0FyY2hjOCtBckptejFEZUZLRzFVODRlY0I4Mk9UQUovQlpFSGhNMXZKV0lhVEVWNUc3ZUJoL0hFYlgyTjdMclk5VTBJbEI4NXl4ck1zTytRUXNOc2N4V0FRTldLcVM0QUdNL2tnSjRoOWdrZkhJR3pKNFFnNlBCVHpTUTZwV20zak5OSlhUdEtHeGl1TndLQ1JZVE96Q3ROd0gyUjZVUVMvZGFxVjhZQzByOGRKY1ZMUWJ1V1hvSjVEcVVxcDVUS2gyM29XcUZ2ZnZHd1UyV3FPRUloKzZkQXJtS3BWTUN5WEtyd1kvWmdIeGQzQTMwK0htUnZ2RkR0VUJ3NXF3dmZBbjNGc3NOcGRGZHlUdDRzdXExSGpvaDlMc0N3dWZ2dm1YWXpmRFZPR1VuZTZZMFNrYnJ3RzVBVG95UzUrK250SEFZZ0VUNUVicnoxT3RQSVNSOGphdlRCMkU2VzI0TFNLSWFJTWg5Z2hXYlhwR1dQRHljVExWQmVzZWx3ZkxxaUpWUlJKenZmK0s2Z204Mm5Na3VWcXZvcGRaRjEwM2p1aTRLeTQyQUVhMzBxRUJna1BiUExvQy9EMlVpczJzYlJtMG9qNUFjdmNNb2dSQlgySkdjM3VKYUFHeXUzQ0lkVGJEYlAwdG8xSFF6Q2oyYnViZmJzRGdaL2JlaEdGSWxwemp4UWhma3F1V0RQcjJOeDV4YlpNUUZUYi9ybTlESkk3VXZBQ2pYcVgzRUdsZWdsd0Z6dzlFYmFHNUthZ1FBUnc2Q3hVb0JGZXpONXdHTWFuYVNpd2VBMHdxWktRZFNYWFBMOWZvL2xZb3VyK1k4ZDNsSFloVlZpVEE5RVcxeEI3WHR1Q3JvVzFkM1lYVFUrWE5OVmdIVXFhbXJUWXkxcVhTN0ZBMXdFQU5MRm5IOFY5RC8zbVlhWC9pTzZsTjBUclA2TjBuVW5Ea1cyWVI5TFJ1YzZBS0RtZWpRQUdOM0dKU2Z6RUVGQ3VaSHZpV0R6c25MdnkwQWhwZXA2ZkowRWVBQ2VDYzdPemVCM1lmd2RvUWdqamFCM1kyWlJHeEVvUG05SzVTa1IwQXhsUllmMzc1eGRFbzBGd1VvNW44bkhQRCtDMjBJVjQxVy9VaFR3YzlTV3lTMFZyQkZTKzdNQ2ZFeW1LcE55U3ZsQWFhQ2xrb3ZlcDdQWDJtVUJOZDB0RzRXellwbDJzWVM3b3ZJVm81NlNEbGVyVUJqdXc1UjVYSG42d1JGaDB1WW5FQWVOb0VHZHZ3b2gyT3pRemZ4eUtqM09BUmdvSVFoWDJnRFNVTkxZSDRWbnNrY1FWd3JsYmxyTGsrdTlhYkdwY1lMNGVEeHRnZDB2YUt4N2JFbEFjeGpPUGZtdHpPanZ4RUVKQzEvUTBYbUErbGJKamtxeU5XNWNpL2NqOFJ1VnlQejBKRklvMFNnc0JYcExzRzFMSVN3ajJRRlNzR3h0Qy9LOXB1R1RyMG9UMVZHVmpjbHdYczFMQnhGd3IzWVdlclZqNkN6SDZ1emlEVzAwL0FobmVJOExabFg4d3RkUGgvV3ZGVGRtZXEvWG0vS3hwRitxa2ljSlRwWnZ5bkRwSUkvKzMrZGYyZndyNXNCcExzY1JUWjRBSU5RN0UxL2pvMGIzdjhib0srWmFFRGR3M3NGTzZCcFJnZWtMWjlBU1BqWFJNSysyOGlqTUJxRU9kdEM2bTN3OXh1T091L2JpSUFDVnpaRmRySTRBNHZwbzhkNUZSUUY3aDVzb3h6SGJFNS85MWtNeUNCRGRHR2txL2ZDMk1uSTlZT0JueHh3OVVDcjBJTTNmRTIrQk5jakhqajFNRXRSU3hTSTZtTHQvQ3A4V1pQSEtkdkZjR3dKZWlKdEFOYSswK0ZLTFhDUTN4K3d4aGpvTzdvSWFVbS9hc3QrN1hQcGkyL2gzR0dyTTJ1WHpJemVhQ3lLYzVPNHBCQVhJZWM4VldNZ3J5bG5vWWUzaFB6SlVwVXp0aEFmV3Qrd2hiVy8wdWJPUlF1SDJLa0VOSjVLRjJEV2ErWFRiQ0Z1TER4WUNSZXh2VkJzWWRrTzlKeU0xTWxTT1BweEx0dU1OZ1VmdG9maHMrTUFUOWdBZWhwdmRYZVlWM05hdUZMQ2ZzdkFuOVNkTXZlb3BKOFpMZEV6NkQzK0ROTlVDaWxxaWd2NjdqNm5EdS9EbHNIZVRvREluUElvVGFORHdyZVRjcHUxa2ZPYkZoaUFMMDdnbFFXSWxsSlRoeTVHSExDZVc5ZGNEdzlzQzdnbXpZUTFONXhXZUYvREpBc3Qvb0tZajVDZVRNc1JYRHJhb0lvUHFIV2ZvOFozY3RZczIrYWZ4WG1PZkdvSmZVYXNTeUkvNCswSSs4WWRoMUhOYllkSmtLOVB5aTlIOEJVNkFmbnYzd3B0azBUdEdrQ09rbGdLZU9LeDJnOTFlOFgvOTk0WnFGL2NHT01iWTJlcDZNNyt4YTU0ZUlFWlJVcEhaeG1ScHRNb25uRG80ZVNRMDBZYkFOUDdJeDFDdXgvOW1xbC9jSnZNOUNWbDFTMTJvck5lV0s5a2ZlRHk2bHpIWktmSk5KZUdPQ3VPVkdDcitaeXljY2JmM2pzU2JvVFhoUDNta2JyeGFFNFBsNmx1V0pSRVZCNWpqRUl2MWdLOG9MazhJN2xOOHUvd0hHNHBCT1VEbTlENDZqVVZlcEp4TWgxMXQvb0w0MjFnR2lKbmc3MFlvTjNCUGRBR2lpZk1wZC85Y2QvaGRHVHIrQjRldjMySElTQm9nRDl5Smg4L2gxaVJpcGhMK1UyUnd4bU4xVkl4d0dZMmViQW92eE1saUxPdUw5aUJGd2Mvd2d6a1ZyVHFseElWK3cxenFFa0dQZmo2UHdJWnNnZVpVdlNKVFRIVVMraEJ0SHl0V1R4Mk83ZVErdk83aXZzenNHaTgwRm5HRWNteUJlT2pKUnA4SDZGdHFjdkpKa1hDdFZMQXNaOTBaTUI2MjByM3dCR2F3d0t5aHFmVldqRDNwcVc4WTBhTVYrRmlxRFZaV084akdMMFNneUNJd1dkSkhWYXpUd2dGMWYrUmxYRGpwNkRwcXFXYi8velJIcXhzZTVhRG1xQkhIYmQ1WDUrbGdVYUpmbnc5NnhRZzBpd0RmbGhtUEMzN214a0txNzNOZi9pWmkwcXZ6ZCszVHRZMnVZZWpTRVlSL2R1ZU5wcUMzd0NNYkRjZFZvK0VlNlBMUnl1bWZ3YUNCRkd3WEp2UDhVK1BJVFliT3lEMzZVQ3FnWTM1TGhmU1E0OURXZ2liVTZKQWtZSTdYdTlsNDZVRW94Zy8xMldRUXRMdk03WVQ4MkprM25kSkpNVFFpWU12VlArT3NBUjFLNjVUVjN3N3pzdDBuK25PbnlYWWFzbFJXVXJYdU1qMHduMy9YMlQ5dkluWHJRWkszcmNiZWsyNWNxYmJQeDVRQXAvUExWTnNqb3Z4NDBmTTcvMWFoaTk3SHVDVktsdm9CUzlvVXlNMnYrSVVWdHAvQmlaWjhYOVl2ZGdNQXpBR3J5T0VlNkFuWFpTRldIVGl3dUtqbFVJQVdOdFV5cnBaSmFoTVhDSzNiQmlqYlQzalNGYU9CSEZVNmpuTyt5VlUxSzBseE1pQUdWeGx1b1VlVXF2Rm1rMlp3OGc1RVBMK243QjJyVEptdXUwNnlBVDVpajBTV3BzZnFBMlZ6cUlNQ05kbGpBdGd6SlNnL2JGN3kyKytyRmF0WTE0SkUwV2NWOWs4VkwvNzhPMXcxSllLTGJTa0JaTmk0ck5TRHI1RFBnUlFFRjRlemtpYSthLzZycTFqdXdqdGdIa2JKOUpGM0RFU3E1WEQ4d3crRXV1eHFNL216b3RSWmR0blh4eTdQdWZtVmVwbkJKTmUvckJ2Q1M4bnJQZ3ZRbHRLa2VLTmdnbWhCWDk4V0pPaDhnVTVJbmNlZ3M2ZTFHMTcvSU5wc2g0bjNneFNJQmRzZkFmVUdiSkt2ZjJiNHlVTXdQWTFZbi8raGVUS3NRKzhGVFA4ZDdhWkZCQzRiWW8xZS9DaE94NTNHK04xZVlFaEZBWWdSdjhIWnQ2SU5td0FtTDRGRmltV0QzUlhnKytXSjRLTHA2R2RVVDBGcjJVT2V6MUtCUnVTQUxDYXE1YmFhYm1Oays4V29YaXRFQnFZYnl0TXFSYzVkMXVVNEd6MFpFTUExNnVuYlRhNlZVTWs4eGY2UzI3Z2xSbG5PTUgxODVUdFZVL3A0N29wTkpUUHQwYjdDNHB0T1p4cnVGY0owMVlTV3lGLzFEdVlOdE5vYnEvZ1lPd2U3R3NCb1BWVHZWMHhhemtLT05qZUVYZURXOHgyUXpzTEswcUlWd0owVzUwWEhYYlk1cnRZcm9mOUVzWm1lSDRudlFxQ1dJci9yT0hxVElWS0krQm92Y1BRSGNlV2hiMDQ0MEpuVnVlSDdqRzRiQzdDVHlIMEROQXZmd0g4R0poem1qdGRtUFpBNG5FRmUxWWUwOHc2UldMUkJ0WXR6b0hiUG4yUlVVYVFzNFdxd0VtejJEcGMxN0RCUmR3cHovbis2Skx2cnNnRThUcG5pbDlZMkIxb0IrREpBMEdWOFdSaHlhRFhoVmFRQmFiem50YUcyVWtpSGErZ1dXZTlHVTYrbllYTHlMSlRMMlZpcEwrWWdtWFVaVGVPbDJKYWlSU0duSFJCR3JyQmU5YmluYlpzUjlCRURUaGwzcys5aDZVVE1oZFFyMHdxSVlRa201RXpNUGxpZzJ4ZU5TcFdnWHV3NDZXQ1c3T0duNEpGSlRpUEw4andmMXdMallFU1dOUGNOaXd4dDRSbHpDUEFuVkppNFBpMzNUelIxVXZ1UDlsYlRZeTVuOTlEWTE4VVUzVFpGNnBzcGRFTndIeXAxelFFZVpWZ1MxOHhLYzdZd08xSU80Q3ZXaWtSMEtubS9wRzdydVJZQjhvSHZMRXJrSkNjbzJDWHdhbEpjUFQ1UGxXU1VzeHRxR0wyeUk3OFJCemZzclVyaCtINi9sMFJJMEVlTTkrTkJ5eWs4WlVBZUgwM3FMN3puRHY0emw1bldmQ3ZncVFxbXdhL05STWIzOC9lam9JenhnRnNvVDZjUUFKY2lML2hZRHI4TXg3UHh5Y1ZJdnExL0ErVk5OM0F1dUlmc3g2NUMxT29XWDVHSkltaVBORHFtRmplS0tHellPMDdzd00wdXpteUJiam1JZ1pJTy9YTlB2QW1xY2VWMkJWcEQ0Z0FyVjljcldFL2hVVU5pSkkrckZSWS9Yb0RpbTFwNGNXcG5CdEhZRWpXMjJRb3JCSFBXU0l0azFBZEE0NVRaZTZZWGlJbzJXNm5EM3F6Y2g3V3UzaGxEZzU3ZnMvMUJxRWRBL0xaeGMrZHZiNmJiUHFIQi9ycmZTTlJXZlJONGJaYkR4N2h3OFBaNDBMbU4vVmRtZUJ2OXMxSzlGMmVRdHFNb0xiNXRxNFFiUU5WRGlOYVYwQ3E3c05LZ0F5bjBxb0U5RElHWkFrdkoxZGhqai8vMmZKeExpWDl1eS9GVmRhOG1vZno0NzJmdmR5YVJKY21pbndEdmhBL1Y4OHFEWktXcTIwZ09qMHhjQmY4NmpIUVVETVhDRmQ2bGE4cjcvV1o1LzZ3NzZteWJMR0ovdkYzR1JmTlNhaG4xbXFSdlBpM2JobHFncEQxbWQ2MnZlR3lnMTdiUXJmNnRselVNTitkQ254dkVpS0F6OGRVcUp6T1MrYTVuRW1vYVRmUHZ2ZEFNcW9iU3FBZTVJZVJTVWFNa3lQcHV0ZE9RUjMwTnJHa1NUNjRoV1A2NHhQQkZyL1R5QUhqMmZrTmtlWUNFY3Njc3ZqT1ZWSDhsS285anBzZlBOaHZMWjRQNUlWU1ZVWExvN0NsaEN1WDlBNGwrMjNkQjI2Ym94Y1IwMGQ0Q2hieUpYaVZCYkk2RjdiWGZCbW81TnVtMEFzSWJ5T3N5NWVGSTBFUXg1WllkYy9mcUk3dGh3Mkd4aTRFMGRRVTlNcUFsYzNTVW1QWlFRclRTL3JpZ3VNY2h1clcwQmxyaCt5MVg2cjByeUhKZEtnSlhmMWc2V0loRXNod3hWZ3BLbGRoTHhnR2xETFBwQWRCOWJuUmQySkpuelNyd2FpSXp2TVA4b0dRVlhHeEFMZHFXbVRZTVlGRlRubW1PT01DQVhrbDVibmQ5S0pOcHRIZ2VOQS9IVmRWakpJbDRmVzY4V09EYzNYVzFLYWUwNlJtSWFZTy9jbEVFU3hGV1Nrd1JJNUJ1U2VsSER4YmY5UkU1blVFR2hMNHg4VDVSclBsY1NkejdQaHBIa0Q3aGtCZGRnKzE2Ym1ETXB0d25zc295Y3RwTVdzcGhUTXJTZmZ2SDJCQmx3MzgyZzNxU0pDMHF0bUFFakdZSzdQa25za0owREhjUEswTldBc0djMStFUVJIckNVS05HKzBJYU8zUHR3TTBpbUNqcFNVVjk2cXBBSXJVazVLNzhLZVdqM2ZheHZBSXJQcmRBME03SGpCeHJUYkhNWFNkNmUybmlSK2ZLdXB3S3M3Zk9kbFUybjY3eTBtWTVVNnlUcGRmTGFsQ0UrLzRkUlhUV3VTRkthN2MvMmZvSjllN3FBQVBCMzN3ZGtaSFRFOGp5UzhZVGIxTUVpRkw4amVzZXpWKzFsY1VjZ3ltRnhsSk1TdE9nR29hSFlveDhPSGVqTmkzUWJ2K0gxRStNSGUxazRic25wMHhmM3dCYmkrcU5BVW92UmVFN2tZUHRTaW90NzJKdWJrQnI0TzQzZmY0dmlnSXpMTFlDWFlOMnd6d0VDUG9rcnY0Slg3VzFlQkdjeUFyN0Q0aWRpN0hDMG92Y1p0OHdROVc0V0ZUV2poSkM4Zy9XZWFpbWx1ZTQvRkM2OTRWVG5mTDM5eXpzb0ZURUFiajlvZkpzZDNBNHpPTElndmdETmdZZzBPMCs2clVzcmRrYy9Wa0E3bVdFbHBiQXBIRng0d080VkJYbG8rb1ljdEQ3bTJ5eVBIUTVZUjVPRDM3aGRxNmtpcTFUNmVrZTFvQkUrc3RDN0FtMGJNTld5aGZUdWxoL2xEcFpxZnBGS3RET1hqS2RhVWRGWDl5L25OZUVoSzMzeVhLbXNJaTNTZ0ZRdzRUd0tSR3IyTmlBYzVPdzVpMEUwdEFLY3RualZpWjEvYUlEakdISmgyRThFTitZT25VS05ZTFVDUDFicmpEdndndnZPc3lhMW1YQXdVanVEdUlHc1RleXBLMkt2RE9HM29UK1FkOWJtczF0d2gwdUIvcUJiNWtpUkk2UzA3WTVTVEF4RXBtdVE4ZnI1RjM0b1VBeGhDTmJ4SnhtN05IZ2dJcnZwbWlSbzVBSE95ZFhpMzZHbVppV1RQbTQ1Tit1YmorejMrcWNJN09EKzNzUXNMaDkrT0lWa3F5WU8rWTRWVWd0TzNwMUtFNXMzb3AzTVJWY0pjWjBVOGpmZVhpb2VldmFKay85SDB4MXlMR1lhbi9PK2ZZQ2kxZ1hpb1hsTEM0STRXN05TLzRZRTBUcjlIT0lpeHFUcTM1b01OOHVLWDFuRG80bWduOU15NlpJTXYvSDA2WEhYVGRaYWg0UHc3bXJlR1NEeTFaVm9OM3FiRHZ4Mk0yUGJwT252RUJyNWJwVkJ1Z3l4dXJ6aTdVQkJmc1VXc2YzRmE2dU1yUnYwL1QxVzcvZkZTN05CeXFEbmZDbDFKYS93V2pvWXE1MzZpZkhRWnZtanQyRnZLZ1dUM1FtWC8ycnIzRmY4SkVTTGYwdllwUkVqTWFhMkZnbkNwNGE0QW9Oc0hLd1g5WlQ1VllxY3VQblJMUk84OGJUNWpGYUk5RGFKQmRHUis0dWp0Y2pHdmZiV2ZKTnFYaWhNeDhjcE0rZm9CM1dIQ0tKckgwbWtYMjcxMFE5QmJJaDVFSEVZUTlKVlcxMUFyYTNFOEtxSkM2MEhOZjFLczFXdnNaQzRYVHJFQ1FWVzYzRWFGd2RRbkRkK3l0dUp4WVJEMlZiSmtHbW9iN0xKSkxmRWFtWG1QbGZKbXpkOTBtS0tTRGFDSjN6MHB0bHF4Q3RtajVlcU9maGFNU3lhTGpwRWo1cHB2WHRPaWpWSWJRUXloNitRL0NkbVg1azIvUHZxaUlsMVVVT3htc3hENTl1K0tST1owREJ1RUZCL0NBVkVZRThrdENvbEVmbGFBc1FTOTVtb2srK1lwRjRLb0FJeWtEc00wTnhXdmF2K3BpWnFsU0FZb1RmbjhFVGZTK1laYjdJcVU2UFkvWFdwWWd0clBjR25MQlB1WlIzZ01rRUgyYmNYVUNvVlZ6em1vTVlVa0NMT2pXYXE3Qm5nQjhEMGxGK2hveVNUUk1SKzFJOGUzeXhtMlU2SUZmR0pZcDRIcFc4TnErMnZzdUtwYUNJRU92OHNQeCsrRkhDaE9YdkdoSy8zTkpoOVc3ek8vaFFRUUh3UWljUmcreE1aUXlzdEhkSDhwWUlqby84anBlRHRSRmExQWdVbkwxdFhHYmlTcEJhVG0zMEpGLzY1Mzh0aWNkMnhtZGZOcTB4dkthNU10aUFTV05zY3NpSSsxMEV0TjRkWmIyRUpvdmtOUGhSQzdzd3NXbDRxVWNUUW1WUXZvemRSbDA2Z1RvWFBPZkdsczY2UkdkUTFtc2ljUDVmWDRWNVFrZHAzNWNodDFwQVRtUEdVOEJNWUNZTi9Nalh4TnVUQVFBdVlJVWRmb3dENTEyNGRoK2pCZXk5L05sbnFLbkptdjZXYS8yY1FacjNkdFlWQ1pYdnBvb0NOeFdXSkpIMzhFS3FiNnN0aTF1SFdJVk5zUU42WktxMFc5WGlrS3o1WmExd1VUNWN0WVpRenJHU3pnbllzY3QvTnJGbDZ6VFR2dngraDRGVUpuSjdYUnp5VWZUcTRVb2JuTllmV2pVbm1udXl3d1FsZ3hueTYyQXdjZnM3OWdGN0dvSzhQZEdlWitocWRCTkhPbnhPbTIrRVdOc211bDNXUVpxMzBWVkZLdlFjZTYrTEQ0QXg2M3YyNTBvUm0vY3dIM3Z5WHN4bVc1c1lmTS9RM3NBYmtIUVZLd3YyTzIxY1JVTlBpMmJYRkJsaURLV2hNUlNOT2RmMzI4aE1RSmE3V3M5SSs3cjgwZ2hvblZKd1U5VTBIU0F3alRpOWc2REhpQ2NBTDNRTFRkZ1JuOEI1d0dNcS9HVXplbjZmblN3cTdNS2ZQVWkrcVJzc0hsNFhTaklNSHN5RDhzOGh3UlVMc3k2TnEvOUU3eWUyRXBRL3hIaExwMFBJbHI5Mzg5UzhNKzgvU3ZRbWJvTTdIL3ZZa1Ivc2pnUGREaTdEdWtORG94TVdjbC9nTHVRQk5aaWFpNFVER2JoWkFNblhQYUlnUUx6cFJYdStJYzhIeXQwcnNlWUFKZUNmYUN2dTJLWUNEcTJVYzgwRU9oYzhObndEd0RGODNFWlcveW5MVHRldTBnNXczM1ZwNkkrZXZmZGczZWdmWFM1WUpFbFZPY2E5VnNJQyt0aVA2QWxzbUhqU1FDZGJzblAvZ0NIeENEMFRUenR0MHE0WjhkZ3NVM0NmK3BVY3lyK0tWdHQrOVlXUk9tMEI1ZFgyRVJSWXZOQU5GbWV5eHVjeEp1TzI1SVNGQy93cDEzUURlZjJnN3ZhR2luMzlDUTdMWmoxcFJIdzY0QTZnb2NYWE5YbFA1Ti9RekhsNzZmc3E2SkFIM3poNldRNXA5UmpaVnVTNndCeTZ3T1o0NVYrenFVYkRCOVh0MDd4MkVVSzNqc3UrSlBSdXd4bnVGeFo1NFpueHVzR1ZOUzhaa3FhRGo2anBDQkZ1aUYzMDVwMmlqbWpiekg4d0hObWFCRy8ybllndTFFWWhJQStlMTREa3pkN1MxRVJyanJocGNWMEpFby80ZDBRMUNKblhwbUV2bVk0WDVqeVI0cnM0S09WSVZkWm9leVBUQ0pORDBWaFZTR1RpcnFPcURkUTZqc0s4a1V2TGVFRHJra0had2FZMnlvY0RCL2pCTVBWTmMrOWl2c1NTZGZjYldqSm9TbXdTTGxvQk0xa1BuWVFJeHdPWkxsaDMzVjEyQ2RpdEhnVHg4SjJ1UWFZK2lUYW5tcE1BWGRCdytDQ3hhN0Z2RFg2V0h5RzBoaVJkUWY1U0huajlGV3VGVXVrS3lMNUVhVjNNcFRNSStjRGNvZFdIVGJWQmFaQ0NONmRCMWxEMVFzNVZvTVk3Yit6UldKUVlGTVdrdWJmNjZWbW41bW9aV0pwVHpUdUFhWEUrRDlUUkxiVms1d0ZPcXhJbXBWekxVdEx1Z1NicENWd2drT2NmTEp2SWtGQkVmdnYyVGp3dDZEZXhKc0tWTk1PUjhsbS92S2Z6MkVyc0F0T3hBTDNRWHRhTXV4cnZGTmpPN2hJdU1NY09ncHAxWnFDaDRkUnRPOXY2QndIcncxaTZtaXF1UXQza1krRmF2U1pEU3plL09KeXlYQTZ3R1JtOUI5c0IrdE0xYm93T2IyRHh0YVZVQzNDS3NOKzFKRVNyZ01QaHpwekxJbFBtQ2VmV1hoUnM1VlZUWnEwRUkyUDN6a1B6eXhYOGRkd0hDR1hJTXgxSCtmZjkvcithMkRNQ1NSR2M2YjB3M01UemFlTllVNGs2RmN2bENLWU1ldldHam9wOUxtdys4dVZkb0ROeHp2dFpiMk1vR0s0N0JQWkpuK0dDSi9ydXhhTEgzbHRUZVhBNkJ6VDRBb0FGcy9SQnlSTDJ2aUVVSVFlZ1NXV0ZiUVNzUm8raFBiQS9QcnJBRzFTd2t1TnJ6TW5XWjZuUnlxSFhkNnoybTlwcjB2RjlFdG5pRng1Y1owalZOUnJqcEhTWFgxM0xnTWVJdWpzaWdITm55L2crVWd2QTRnMkdnbjVRb09va1NuWWFDdUhVQWhJd1hnVWx4TGhQeGZXNUpZMWtlcFhlSTFsVzlSV2RLSnNMNUJsMW9ZVDBuaERmemZBZklhNjY5ZjQ2Kyt1VWNFbjQrcFA2Vk84SUs5cSt1VjRjbUl5dSt5ZjIrejdBdGFvMGovREdxMGQwYTRUMjdVeWM3Zm50bDJ4K3loUHdPWENETU1lMENDaEg4bHpqOFBhQWxrOG4wVjlkTXlkSGtkTEdOMTFWKy8zYXoxaTQweUF1Mnk2N0lVV1preUFLaWtKSVEvelZyRHlVbFhQRk9FSzBISVM2S3JUTkRISUdpam54YzhLNVVMenpXQ1pva2dHdUZDcG5DOU1HS3d5OTlPSVQzL005ajU5Z09pOGVmdWtERkJZeUYyLy9RM3doaFpMcVFCNkxwQnBnQnhGRkR1Q0ZXYkNJWUZzM3NUczg2NnJqZUZRVUVQRU1URWRwQXpEdk4zVUsycDVRMXVtT2VUcXo2eGh2VlNpWTZqQ1pYREVIM2VQU1RwZFBaUTZ4Q0ZscUdHdHg5L2pUOUR1Rzk2VVkybGVIaVVJRDlXWis2c1p1NjkrZjNVa1kwY0xLN0krUVhzcWdQTStCbzdZYkRvYnAvRktQb2pSamVhWGR5REpVR3VYRFJ2RGNRZEh3Y2hnL05FK3FyU0dCZzBiRXhQRkRRbzNyaGIrQWxQR3NFbTNPZWkzd2ZPM3FYRjVJNjNKRHIrVE9JVElwTmVTUmhIZVV2Y05yek5BN1dFQ09vZTJPYWZwVW1SUkRMTVMrNVFvdHVTdnFraW9wYm1aemNudjd3eE05ZFlWSGYvL09qMU1LVVptbnZ0SnROcXBjcWxTUk1Ca1JxQWNnZElFVG5hRGFZOEZvYzBUSGRZcXR3NTMwK2JENnpCTkFRSGRoNVhBMnNXbDFab3lXZXpTd29ydENSVkpONkZsVFc2Y29oT0cvaDJqNVY0WlRvMHdLRHZtQS9rYlZuZm5PalhaNW5qNEIzQXk4SGJPVmpML1NlUW5GYWd5QzhJSGVUN0dwaEY5NlM0Tlo2eGxWb1gwV2UwZnFWS0dERUxUcXIyRDRFcDU0RXpWSEhoSlJ5OHpCTVNuZDFNSjJYd2FLeU1jcDk4Y05Bc29CaXlRUythUTdtWmtnTUduOExUdjIvWkROdXl4VzFJcUxmdmgrWnZuTS9SOENwaEpkRjZKZ0ozVjF2bkh4cDA2Rm9ZZGhROUFyK2dYcXRjeFFPL2pXcFJERS9KaTBsZ3RKNjJMcWVYRTh6TjhzRTQwWDJTaHUzdVRpWXJxbEVic3NJa2Z5UFJqRjdJSjlSS3BQRzFPdDlNWjdoUW5ZQlhlQndubllBMk5jUmtla2V2SWp1em1KRVR4bWZQczNVS0lDaVcwRmlWUmxseTJma1ovWi8xb1BRMUsvTWhmWkRWbnl6b0pwOXdYMlBzejlVenMwK2NYNlVGR2syY1hCNWRBOENTdExhdDI0bXZEMFFDc2RsWm1wVi9YQTNPTHdrbEVsVUdvY2ZCMDYxbzR1RjdMbGFhUFNEcnplbm9pZkY2cmE0QTIyNk5KRms4cEN6OHRqUHV2UHE5OVN4SWpoUUVWMDZacDZFZnJkM1YrRnBBbWlCalVJUFViOGc4aEw1VUo5VmwyZjJJa3hHMmt5QlEzSStmMFdnU2R6RWxlVnBwM2xIUHBGN3JobGZ0TTR3Z0VFZGRheHhKQTFoZjVuaE9lT0IzTDZpY3ZtZVpCeUIxV1hHOHdlSmpTV3FuZm9PZXJ1S0l4RG9OMDE1aTdnTWFNUy9uVEY0UjAwSDhQTjFHVkVRV1VveTFXbVd1ZzgvOWFPRzNEOXFYL0hrS3BoN3pmek5DeUE3bGlGK3RKbnZlUHJCZ2tvSmwxaE8wcHdSYVlOMnc0dXpTT25UNngvS2VDUkhVazJWWHN2K1Y4KzBRSVZkOWp2RjV3bjh6RXlDbDkrb1pNSGZ3djdUN0FZeGprS2Y0QzZCOUwybGZUVUtzMktJYUZGQ1p1eVhnZ3hVMkswemZtU3FzMHR3UTRqcEticlRMc2k3QVRlS3dGUjVzLzl5ZTB6QmxEbURMYUQ2czFIZDhlQjZJaEVmQUZyTjlYVVpEV0w2NWgzaWc4UDIydGZBNStNMFFMeElrMlNqU3FnUWFOM3QvRWxjY1RmVmtkazNKdDdFaFJLVVQ3RGNCeDhpblNtNWRlTVBpc05sVTg2WGc9PSJ9
-- CEETOS_RELEASE_SIGNATURE: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
-- CEETOS_RELEASE_PAYLOAD_BEGIN
-- CeetOS installer. Generated; do not edit.
local files = {
  ["startup.lua"] = "local pendingPath,releasePath=\"/ceetos-updates/pending.lua\",\"/ceetos-updates/release.lua\"local pending if fs.exists(pendingPath)then local loader=loadfile(pendingPath)if loader then local ok,value=pcall(loader)if ok and type(value)==\"table\"then pending=value end end end local function checksum(text)local hash=2166136261 for index=1,#text do hash=(hash*31+text:byte(index))%4294967296 end return string.format(\"%08x\",hash)end local function recoveryAgent()local root,descriptor=\"/ceetos-dev/recovery\",\"/ceetos-dev/recovery/slots.lua\"local source=\"/ceetos/recovery/agent.lua\"if not fs.exists(source)then return nil end if not fs.exists(root)then fs.makeDir(root)end local slots={}if fs.exists(descriptor)then local handle=fs.open(descriptor,\"r\")if handle then slots=textutils.unserialise(handle.readAll())or{};handle.close()end end slots.active=slots.active==\"a\"and\"a\"or\"b\"local inactive=slots.active==\"a\"and\"b\"or\"a\"local candidatePending=false local transactionPath=root..\"/transaction.lua\"if fs.exists(transactionPath)then local transactionFile=fs.open(transactionPath,\"r\")local transaction=transactionFile and textutils.unserialise(transactionFile.readAll())or nil if transactionFile then transactionFile.close()end candidatePending=type(transaction)==\"table\"and transaction.state==\"candidate\"end local input=fs.open(source,\"r\")local text=input and input.readAll()or nil if input then input.close()end if not text then return nil end local candidate=root..\"/agent-\"..inactive..\".lua\"if not candidatePending and(slots[inactive]or\"\")~=checksum(text)and load(text,candidate,\"t\")then local temporary=candidate..\".tmp\"local output=fs.open(temporary,\"w\")if output then output.write(text);output.close();if fs.exists(candidate)then fs.delete(candidate)end;fs.move(temporary,candidate);slots[inactive]=checksum(text)end end local active=root..\"/agent-\"..slots.active..\".lua\"local fallback=root..\"/agent-\"..inactive..\".lua\"local activeSlot,fallbackSlot=slots.active,inactive local function validSlot(path,expected)if not fs.exists(path)then return false end local handle=fs.open(path,\"r\");local body=handle and handle.readAll()or nil if handle then handle.close()end return body~=nil and(expected==nil or expected==checksum(body))and load(body,path,\"t\")~=nil end if not validSlot(active,slots[slots.active])then if validSlot(fallback,slots[inactive])then active,fallback,slots.active=fallback,active,inactive activeSlot,fallbackSlot=inactive,activeSlot else return nil end end local out=fs.open(descriptor..\".tmp\",\"w\")if out then out.write(textutils.serialise(slots));out.close();if fs.exists(descriptor)then fs.delete(descriptor)end;fs.move(descriptor..\".tmp\",descriptor)end return{primary=active,fallback=validSlot(fallback,slots[fallbackSlot])and fallback or nil}end local recovery=recoveryAgent()if pending and pending.state==\"apply\"and fs.exists(releasePath)then shell.run(releasePath,\"--ceetos-apply\")elseif recovery then local ok=shell.run(recovery.primary)if ok==false and recovery.fallback then shell.run(recovery.fallback)end elseif fs.exists(\"/ceetos/startup.lua\")then shell.run(\"/ceetos/startup.lua\")else shell.run(\"shell\")end",
  ["ceetos/startup.lua"] = "local root=\"/ceetos\"local handoffPath=\"/ceetos-dev/recovery/restart-request.lua\"package.path=\"/?.lua;/?/init.lua;\"..package.path while true do if fs.exists(handoffPath)then fs.delete(handoffPath)end local profile={id=\"desktop\"}local loaded,profileService=pcall(require,\"ceetos.lib.profile\")if loaded and type(profileService)==\"table\"and type(profileService.current)==\"function\"then local ok,value=pcall(profileService.current)if ok and type(value)==\"table\"then profile=value end end if profile.id~=\"desktop\"and fs.exists(root..\"/server.lua\")then shell.run(root..\"/server.lua\")elseif fs.exists(root..\"/ceet.lua\")then shell.run(root..\"/ceet.lua\")else printError(\"CeetOS is incomplete; starting CraftOS shell.\")shell.run(\"shell\")end sleep(0.1)end",
  ["ceetos/profile.lua"] = "return {schema=1,id=\"desktop\"}",
  ["ceetos/ceet.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path require(\"ceetos.lib.runtime_modules\").refresh()local auth=require(\"ceetos.lib.auth\")local authTransportPolicy=require(\"ceetos.lib.auth_transport_policy\")local devbridgePolicy=require(\"ceetos.lib.devbridge_policy\")local accountSync=require(\"ceetos.lib.account_sync\")local net=require(\"ceetos.lib.net\")local peripherals=require(\"ceetos.lib.peripherals\")local telemetry=require(\"ceetos.lib.telemetry\")local audit=require(\"ceetos.lib.audit\")local store=require(\"ceetos.lib.store\")local supervisor=require(\"ceetos.lib.supervisor\")local sounds=require(\"ceetos.lib.sounds\")local jobs=require(\"ceetos.lib.jobs\")local crafting=require(\"ceetos.lib.crafting\")local craftingService=require(\"ceetos.lib.crafting_service\")local craftSources=require(\"ceetos.lib.craft_sources\")local networkIpc=require(\"ceetos.lib.network_ipc\")local networkRuntime=require(\"ceetos.lib.network_runtime\")local updater=require(\"ceetos.lib.updater\")local profile=require(\"ceetos.lib.profile\")local cloudSync=require(\"ceetos.lib.cloud_sync\")local cloudService=require(\"ceetos.lib.cloud_service\")local releaseBroker=require(\"ceetos.lib.release_broker\")local okVersion,version=pcall(require,\"ceetos.lib.version\")version=okVersion and version or{string=\"0.19.50\"}local authAuthority=profile.capability(\"authAuthority\")and require((\"ceetos.lib.auth_authority\"))or nil local authAuthorityRuntime=authAuthority and require((\"ceetos.lib.auth_authority_runtime\"))or nil local SHELL_REPLY_PATH=\"/ceetos/data/shell-replies.lua\"local CRAFT_REPLY_PATH=\"/ceetos/data/craft-replies.lua\"local CRAFT_REPLY_LIMIT=16 local REMOTE_READ_TIMEOUT=90000 local DEV_NETWORK_STATUS=\"/ceetos/data/network-status.lua\"local DEV_NETWORK_HEALTH=\"/ceetos/data/network-service.lua\"local DEV_NETWORK_IPC_DIAG=\"/ceetos/data/network-ipc-status.lua\"local OPERATION_STATUS=\"/ceetos/data/operation-status.lua\"local SERVICE_SUPERVISOR=\"/ceetos/data/service-supervisor.lua\"local JOB_INBOX=\"/ceetos/data/job-inbox.lua\"local JOB_OUTBOX=\"/ceetos/data/job-outbox.lua\"local JOB_EVENT=\"ceetos_job_inbox\"local CRAFT_STATE=\"/ceetos/data/crafting-state.lua\"local CRAFT_COMMANDS=\"/ceetos/data/crafting-commands\"local CRAFT_COMMAND_RESULTS=\"/ceetos/data/crafting-command-results\"local CRAFT_COMMAND_LEGACY=\"/ceetos/data/crafting-commands.lua\"local CRAFT_COMMAND_EVENT=craftingService.COMMAND_EVENT local CRAFT_COMMAND_BOOT_AT=os.epoch(\"utc\")local CRAFT_RECIPES=\"/ceetos/data/recipes.lua\"local PROFILE_CACHE=\"/ceetos/data/profile-cache.lua\"local lastJobTemplateSync=0 local lastProfileAdvert=0 local craftRecoveryChecked=false local diagnosticSnapshots={}local craftRuntimeState,craftPersisted,craftPersistedVerifiedAt=nil,nil,0 local CRAFT_PERSIST_VERIFY_MS=5000 local craftRuntime={state=\"starting\",stage=\"boot\",tick=0,at=os.epoch(\"utc\")}local finaliseCraftCommands local function readDevBuildRecord(path)if type(path)~=\"string\"or not fs.exists(path)then return{}end local handle=fs.open(path,\"r\")if not handle then return{}end local raw=handle.readAll()handle.close()local ok,value=pcall(textutils.unserialise,raw)return ok and type(value)==\"table\"and value or{}end local function devCraftProbeAllowed(command)if not devbridgePolicy.isCraftProbeCommand(command)then return false end return devbridgePolicy.allowsUnattendedCraftProbe(readDevBuildRecord(\"/ceetos-dev/connection.lua\"),readDevBuildRecord(\"/ceetos-dev/recovery/developer-mode.lua\"),profile.id(),os.epoch(\"utc\"))end local function writeDevValue(path,value)local encoded,serialised=pcall(textutils.serialise,value,{compact=true})if not encoded then return false,tostring(serialised)end if diagnosticSnapshots[path]==serialised then return true end local ok,err=store.write(path,value)if ok then diagnosticSnapshots[path]=serialised local domains={[DEV_NETWORK_STATUS]=\"network\",[DEV_NETWORK_IPC_DIAG]=\"network-ipc\"}if domains[path]then pcall(os.queueEvent,\"ceetos_state_changed\",domains[path],value.revision or value.updated or os.epoch(\"utc\"))end end return ok,err end local function recordServiceTransition(name,status)local state=store.read(SERVICE_SUPERVISOR,{schema=1,services={}})if type(state)~=\"table\"then state={schema=1,services={}}end if type(state.services)~=\"table\"then state.services={}end state.schema,state.services[name],state.updated=1,status,os.epoch(\"utc\")store.write(SERVICE_SUPERVISOR,state)if status.state==\"backoff\"then sounds.play(\"warning\")pcall(audit.log,\"system\",\"service.restarting\",{service=name,failures=status.failures,delay=status.delay,error=status.error})elseif status.state==\"running\"and(tonumber(status.failures)or 0)>0 then sounds.play(\"reconnect\")end end local function craftDefault()return{config={ticker=nil,requester=nil,recipeServer=nil,stockName=nil,requesterName=nil,orderAddress=\"Order\",timeout=120},sources={tickers={},requesters={},recipeServers={},updated=0},stock={},stockUpdated=0,stockRefreshRequested=false,cart={},craftMode=false,queue={},retries={},history={},sequence=0,acknowledgements={},commandLedger={},recipeCatalog={query=\"\",rows={},total=0,updated=0},tagOptions=nil,tagChoices={},}end local function craftState()if craftRuntimeState then return craftRuntimeState end local value=store.read(CRAFT_STATE,craftDefault())local defaults=craftDefault()for key,fallback in pairs(defaults)do if value[key]==nil then value[key]=fallback end end value.acknowledgements=type(value.acknowledgements)==\"table\"and value.acknowledgements or{}value.commandLedger=type(value.commandLedger)==\"table\"and value.commandLedger or{}for key,fallback in pairs(defaults.config)do if value.config[key]==nil then value.config[key]=fallback end end if not value.config.ticker and value.config.stockName then value.config.ticker={transport=\"local\",name=value.config.stockName}end if not value.config.requester and value.config.requesterName then value.config.requester={transport=\"local\",name=value.config.requesterName}end if not craftRecoveryChecked then craftRecoveryChecked=true if value.active then local active,item=value.active,value.active.items and value.active.items[value.active.index]value.history[#value.history+1]={id=active.id,status=\"interrupted-unconfirmed\",name=item and item.name,requested=active.requested or(item and item.count),accepted=active.accepted,dispatched=active.delivered or 0,destination=active.destination,reason=\"CeetOS restarted; request was not replayed\",finished=os.epoch(\"utc\")}while#value.history>24 do table.remove(value.history,1)end value.active,value.nextRefresh,value.recoveryNotice=nil,0,\"An active Create request was interrupted and not replayed.\"store.write(CRAFT_STATE,value)end end craftRuntimeState=value return value end local function craftCommandsNeedFinalising(completed)return type(completed)==\"table\"and(completed.legacy==true or#(completed.files or{})>0)end local function craftPersistentSnapshot(value)return crafting.durableQueueSnapshot(value)end local function saveCraft(value,completedCommands)craftRuntimeState=value local persistence=craftRuntime.persistence or{attempts=0}craftRuntime.persistence=persistence persistence.attempts=math.max(0,math.floor(tonumber(persistence.attempts)or 0))+1 local active=type(value)==\"table\"and value.active or nil if not craftCommandsNeedFinalising(completedCommands)and type(active)==\"table\"and active.phase==\"craft\"and not crafting.requesterCheckpointDue(active.pendingCraftRequests,active.nextCraftCheckpoint,os.epoch(\"utc\"))then persistence.action,persistence.at=\"checkpoint-deferred\",os.epoch(\"utc\")return true end local snapshot=craftPersistentSnapshot(value)local encodedOk,encoded=pcall(textutils.serialise,snapshot,{compact=true})if not encodedOk then persistence.action,persistence.ok,persistence.at=\"encode\",false,os.epoch(\"utc\")persistence.error=tostring(encoded):sub(1,180)craftRuntime.persistenceError=\"could not serialise Crafting Queue state: \"..persistence.error return false,tostring(encoded)end local persistedNow=os.epoch(\"utc\")local persistenceAction=crafting.persistenceAction(craftPersisted,nil,persistedNow,craftPersistedVerifiedAt,CRAFT_PERSIST_VERIFY_MS)if persistenceAction==\"verify\"then local durable=store.readStable(CRAFT_STATE,nil)local durableOk,durableEncoded=pcall(textutils.serialise,durable,{compact=true})persistenceAction=crafting.persistenceAction(craftPersisted,durableOk and durableEncoded or nil,persistedNow,craftPersistedVerifiedAt,CRAFT_PERSIST_VERIFY_MS)craftPersistedVerifiedAt=persistedNow if persistenceAction==\"write\"then craftPersisted=nil craftRuntime.persistenceRepairAt=persistedNow end end if persistenceAction==\"skip\"and craftPersisted==encoded then persistence.action,persistence.ok,persistence.at,persistence.error=\"skip\",true,persistedNow,nil if completedCommands and finaliseCraftCommands then finaliseCraftCommands(completedCommands,value)end return true end persistence.action,persistence.at=\"write\",persistedNow snapshot.revision=math.max(0,math.floor(tonumber(snapshot.revision)or 0))+1 value.revision=snapshot.revision local revisedOk,revisedEncoded=pcall(textutils.serialise,snapshot,{compact=true})if not revisedOk then return false,tostring(revisedEncoded)end encoded=revisedEncoded local ok,err=store.write(CRAFT_STATE,snapshot)if ok then craftRuntime.persistenceError=nil craftPersisted=encoded craftPersistedVerifiedAt=persistedNow persistence.ok,persistence.error,persistence.writtenAt=true,nil,persistedNow if type(active)==\"table\"and active.phase==\"craft\"and(tonumber(active.pendingCraftRequests)or 0)>0 then active.nextCraftCheckpoint=os.epoch(\"utc\")+500 elseif type(active)==\"table\"then active.nextCraftCheckpoint=nil end if completedCommands and finaliseCraftCommands then finaliseCraftCommands(completedCommands,value)end pcall(os.queueEvent,\"ceetos_state_changed\",\"crafting\",snapshot.revision)else craftRuntime.persistenceError=tostring(err or\"could not persist Crafting Queue state\"):sub(1,180)persistence.ok,persistence.error=false,craftRuntime.persistenceError end return ok,err end local function craftRecipes()if not profile.capability(\"recipes\")then return nil,nil,\"Craft mode requires a reachable Recipe Server\"end local recipeDb=require((\"ceetos.lib.recipe_db\"))local base,databaseOrErr=recipeDb.loadBase()if not base then return nil,nil,databaseOrErr end local database,overlay=databaseOrErr,{recipes={},tags={}}if fs.exists(CRAFT_RECIPES)then local file=fs.open(CRAFT_RECIPES,\"r\");local raw=file and file.readAll()or nil if file then file.close()end local parsed,err=crafting.parseRecipeOverlay(raw or\"\")if not parsed then return nil,nil,err end overlay=parsed end return recipeDb.mergeOverlay(base,overlay.recipes),recipeDb.mergeTags(database.tags,overlay.tags),nil,overlay end local function peerSourceCandidates()local tickers,requesters={},{}for _,peer in ipairs(net.peers())do local cache=store.read(\"/ceetos/data/remote/\"..tostring(peer.id)..\".lua\",{})for _,item in ipairs(cache.peripherals or{})do local source={transport=\"peer\",peer=tostring(peer.id),name=item.name,type=item.type,mode=item.mode,label=\"Peer \"..tostring(peer.label or peer.id)..\" / \"..tostring(item.name)}local kind=tostring(item.type or\"\"):lower()if kind:find(\"stockticker\",1,true)then tickers[#tickers+1]=source elseif kind:find(\"redstonerequester\",1,true)then requesters[#requesters+1]=source end end end return tickers,requesters end local function recordProfile(peerId,body)if type(body)~=\"table\"or type(body.profile)~=\"string\"or not profile.IDS[body.profile]then return false end local cache=store.read(PROFILE_CACHE,{})cache[tostring(peerId)]={profile=body.profile,version=tostring(body.version or\"\"):sub(1,24),seen=os.epoch(\"utc\")}store.write(PROFILE_CACHE,cache)return true end local function refreshCraftSources(state,queryPeers)local tickers,requesters=craftSources.discoverLocal()local recipeServers={}local peerTickers,peerRequesters=peerSourceCandidates()for _,item in ipairs(peerTickers)do tickers[#tickers+1]=item end for _,item in ipairs(peerRequesters)do requesters[#requesters+1]=item end local profiles=store.read(PROFILE_CACHE,{})for _,peer in ipairs(net.peers())do local advertised=profiles[tostring(peer.id)]if advertised and advertised.profile==\"recipe-server\"then recipeServers[#recipeServers+1]={transport=\"peer\",peer=tostring(peer.id),label=\"Recipe Server \"..tostring(peer.label or peer.id)}end end if cloudSync.recipeOnline()then recipeServers[#recipeServers+1]={transport=\"cloud\",label=\"Cloud Recipe Server\"}end table.sort(recipeServers,function(a,b)local left,right=a.transport==\"cloud\",b.transport==\"cloud\"if left~=right then return not left end return tostring(a.label)<tostring(b.label)end)state.sources={tickers=tickers,requesters=requesters,recipeServers=recipeServers,updated=os.epoch(\"utc\")}if not state.config.recipeServer and#recipeServers>0 then local source=recipeServers[1]state.config.recipeServer={transport=source.transport,peer=source.peer,label=source.label}end if queryPeers then for _,peer in ipairs(net.peers())do net.request(peer.id,\"peripheral_list\",{},{safe=true});net.request(peer.id,\"profile_query\",{},{safe=true})end end end local function sourceAvailable(source,needle)source=craftSources.kind(source)if not source then return false end if source.transport==\"peer\"then local tickers,requesters=peerSourceCandidates()local candidates=needle==\"stockticker\"and tickers or requesters for _,candidate in ipairs(candidates)do if craftSources.key(candidate)==craftSources.key(source)then return true end end return false end return peripheral.isPresent(source.name)and tostring(peripheral.getType(source.name)or\"\"):lower():find(needle,1,true)~=nil end local function pushCraftHistory(state,row)state.history[#state.history+1]=row while#state.history>24 do table.remove(state.history,1)end end local function failCraft(state,reason)local active=state.active local message=tostring(reason or\"unspecified crafting queue failure\")state.error=message if active then local item=active.items and active.items[active.index]pushCraftHistory(state,{id=active.id,status=\"failed\",name=item and item.name,requested=item and item.count,accepted=active.accepted,delivered=active.delivered,destination=active.destination,error=message,finished=os.epoch(\"utc\")})end state.active=nil end local function cancelCraft(state)local active=state.active if not active then return false end local item=active.items and active.items[active.index]pushCraftHistory(state,{id=active.id,status=crafting.cancellationStatus(active.issued),name=item and item.name,requested=active.requested or(item and item.count),accepted=active.accepted,delivered=active.delivered or 0,remaining=active.remaining,destination=active.destination,issued=active.issued==true,reason=active.issued and\"Create package may still arrive\"or\"Cancelled before dispatch\",finished=os.epoch(\"utc\"),})state.active=nil return true end local function completeCraftItem(state)local active=state.active;active.index=active.index+1;active.plan,active.step,active.requested,active.accepted,active.delivered,active.remaining,active.deadline,active.issued,active.issuedKind,active.issuedAt,active.requesterConfigured,active.requesterStrict,active.waitingFor,active.expectedOutput,active.craftStep,active.pendingCraftRequests,active.craftBaseline,active.craftBatchBaseline,active.nextCraftDispatch,active.packageIndex,active.packageRequest,active.peerRequest,active.planRequest=nil,1,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil active.packageRemaining,active.stepAddress,active.stepRoute,active.packageCount=nil,nil,nil,nil active.craftIssued,active.lastCraftDispatch,active.nextCraftCheckpoint=nil,nil,nil if active.index>#active.items then state.active=nil else active.phase=\"prepare\"end end local function applyCraftCommand(state,command)local status,detail=\"ignored\",nil if command.action==\"catalog\"and type(command.query)==\"string\"and#command.query<=64 and not command.query:find(\"[%c]\")then local offset=math.max(0,math.floor(tonumber(command.offset)or 0))state.recipeCatalog={query=command.query,offset=offset,rows={},total=0,pending=true,updated=os.epoch(\"utc\")}state.nextCatalogRefresh,status=0,\"ok\"elseif command.action==\"tag_options\"and type(command.item)==\"string\"and command.item:match(\"^[%w_.:%-]+$\")then state.tagOptions={item=command.item,pending=true,requested=os.epoch(\"utc\")}state.nextTagOptionsRefresh,status=0,\"ok\"elseif auth.allowed(\"operator\")or devCraftProbeAllowed(command)then if command.action==\"add\"and type(command.name)==\"string\"and tonumber(command.count)and command.count>0 then crafting.addToCart(state.cart,command.name,command.count);status=\"ok\"elseif command.action==\"remove\"then table.remove(state.cart,tonumber(command.index)or 0);status=\"ok\"elseif command.action==\"clear_cart\"then state.cart={};status=\"ok\"elseif command.action==\"edit\"and state.cart[tonumber(command.index)or 0]then state.cart[tonumber(command.index)].count=math.max(1,math.floor(tonumber(command.count)or 1));status=\"ok\"elseif command.action==\"mode\"then if command.value==true and not sourceAvailable(state.config.requester,\"redstonerequester\")then detail=\"Craft mode needs an available Redstone Requester source\"else state.craftMode=command.value==true;status=\"ok\"end elseif command.action==\"destination\"and type(command.value)==\"string\"and#command.value>0 and#command.value<=64 then state.config.orderAddress=command.value;status=\"ok\"elseif command.action==\"tag_choice\"and type(command.item)==\"string\"and command.item:match(\"^[%w_.:%-]+$\")and type(command.tag)==\"string\"and command.tag:match(\"^#[%w_.:%-]+$\")then local value=command.value if value~=false and(type(value)~=\"string\"or not value:match(\"^[%w_.:%-]+$\"))then detail=\"invalid tagged item\"else state.tagChoices[command.item]=state.tagChoices[command.item]or{}if value==false then state.tagChoices[command.item][command.tag]=nil else state.tagChoices[command.item][command.tag]=value end local _,choiceErr=crafting.normaliseTagChoices(state.tagChoices[command.item])if choiceErr then detail=choiceErr else status=\"ok\"end end elseif command.action==\"submit\"and#state.cart>0 and not state.active then local queuedItems={}for _,row in ipairs(state.cart)do local choices=crafting.normaliseTagChoices((state.tagChoices or{})[row.name])or{}queuedItems[#queuedItems+1]={name=row.name,count=math.floor(tonumber(row.count)or 0),tagChoices=choices}end state.sequence=state.sequence+1 state.queue[#state.queue+1]={id=\"craft-\"..tostring(os.epoch(\"utc\"))..\"-\"..tostring(state.sequence),items=queuedItems,craftMode=state.craftMode,destination=state.config.orderAddress}state.cart,state.error={},nil;status=\"ok\"elseif command.action==\"retry\"and state.retries[tonumber(command.index)or 0]and not state.active then local retry=table.remove(state.retries,tonumber(command.index));state.queue[#state.queue+1]=retry;status=\"ok\"elseif command.action==\"cancel_active\"and state.active then cancelCraft(state);status=\"ok\"elseif command.action==\"refresh\"then state.stockRefreshRequested,state.nextRefresh,state.nextSourceRefresh,state.nextCatalogRefresh=true,0,0,0 status=\"ok\"elseif command.action==\"source\"and(command.target==\"requester\"or command.target==\"recipes\")and command.value==false then if command.target==\"recipes\"then state.config.recipeServer,state.nextCatalogRefresh,state.recipeCatalog=nil,0,{query=\"\",rows={},total=0}else state.config.requester,state.craftMode=nil,false end state.nextRefresh=0;status=\"ok\"elseif command.action==\"source\"and command.target==\"recipes\"then local source=type(command.value)==\"table\"and command.value or nil if source and source.transport==\"peer\"and type(source.peer)==\"string\"then state.config.recipeServer,state.nextCatalogRefresh,state.recipeCatalog={transport=\"peer\",peer=source.peer,label=source.label},0,{query=\"\",rows={},total=0};status=\"ok\"elseif source and source.transport==\"cloud\"then state.config.recipeServer,state.nextCatalogRefresh,state.recipeCatalog={transport=\"cloud\",label=\"Cloud Recipe Server\"},0,{query=\"\",rows={},total=0};status=\"ok\"else detail=\"invalid recipe server\"end elseif command.action==\"source\"and(command.target==\"ticker\"or command.target==\"requester\")then local source=craftSources.kind(command.value)if source and type(source.name)==\"string\"then state.config[command.target]=source if command.target==\"ticker\"then state.stockRefreshRequested=true end state.nextRefresh=0 status=\"ok\"else detail=\"invalid source\"end else detail=\"action unavailable\"end else detail=\"permission denied\"end if type(command.token)==\"string\"and#command.token<=80 then state.acknowledgements[command.token]={ok=status==\"ok\",error=detail,at=os.epoch(\"utc\")}end end local function takeCraftReply(requestId)if not requestId then return nil end store.invalidate(CRAFT_REPLY_PATH)local replies=store.read(CRAFT_REPLY_PATH,{})local reply=replies[requestId]if reply then replies[requestId]=nil;store.write(CRAFT_REPLY_PATH,replies)end return reply end local function recordCraftReply(body,from)if type(body)~=\"table\"or type(body.request_id)~=\"string\"or#body.request_id>120 then return false end local replies=store.read(CRAFT_REPLY_PATH,{})replies[body.request_id]={ok=body.ok==true,result=body.result,received=os.epoch(\"utc\"),from=body.origin or from,}crafting.pruneReplyMailbox(replies,CRAFT_REPLY_LIMIT)return store.write(CRAFT_REPLY_PATH,replies)end local function newCraftRequestId(prefix)return prefix..\":\"..tostring(os.getComputerID())..\":\"..tostring(os.epoch(\"utc\"))..\":\"..tostring(math.random(1000,9999))end local function requestPeerCraft(source,typeName,body)local requestId=newCraftRequestId(typeName)body.request_id=requestId local ok,err=net.request(source.peer,typeName,body)return ok and requestId or nil,err end local function refreshCraftStock(state,source,now)if not source then return false,\"No Stock Ticker source selected\"end if source.transport~=\"peer\"then if not sourceAvailable(source,\"stockticker\")then return false,\"Selected Stock Ticker is unavailable\"end local ticker=peripheral.wrap(source.name)local stock,err=crafting.readTickerStock(ticker)if not stock then return false,err end state.stock,state.stockUpdated,state.error=stock,now,nil return true end local pending=state.stockRequest if pending then local reply=takeCraftReply(pending.id)if reply then state.stockRequest=nil if not reply.ok then return false,tostring(reply.result or\"peer Stock Ticker request failed\")end state.stock,state.stockUpdated,state.error=crafting.recipeStockSnapshot(reply.result),now,nil return true end if now-pending.sent>REMOTE_READ_TIMEOUT then state.stockRequest=nil;return false,\"Peer Stock Ticker did not respond\"end return false,nil end local requestId,err=requestPeerCraft(source,\"craft_stock_request\",{name=source.name})if not requestId then return false,err end state.stockRequest={id=requestId,sent=now}return false,nil end local function sanitiseRecipeCatalog(result,query,offset,now)if type(result)~=\"table\"or type(result.rows)~=\"table\"then return nil,\"recipe catalogue was malformed\"end local rows={}for index,row in ipairs(result.rows)do if index>64 then return nil,\"recipe catalogue was too large\"end if type(row)~=\"table\"or type(row.name)~=\"string\"or not row.name:match(\"^[%w_.:%-]+$\")then return nil,\"recipe catalogue contained an invalid item\"end local tags={}for _,tag in ipairs(type(row.tags)==\"table\"and row.tags or{})do if type(tag)~=\"string\"or not tag:match(\"^#[%w_.:%-]+$\")then return nil,\"recipe catalogue contained an invalid tag\"end tags[#tags+1]=tag end table.sort(tags)rows[#rows+1]={name=row.name,tags=tags}end local cleanOffset=math.max(0,math.floor(tonumber(result.offset)or offset or 0))return{query=query or\"\",offset=cleanOffset,rows=rows,total=math.max(#rows+cleanOffset,math.floor(tonumber(result.total)or(#rows+cleanOffset))),truncated=result.truncated==true,updated=now}end local function refreshRecipeCatalog(state,source,recipes,now)local current=type(state.recipeCatalog)==\"table\"and state.recipeCatalog or{query=\"\",offset=0,rows={}}local query=type(current.query)==\"string\"and current.query or\"\"local offset=math.max(0,math.floor(tonumber(current.offset)or 0))if source and source.transport==\"peer\"then local pending=state.catalogRequest if pending then local reply=takeCraftReply(pending.id)if reply then state.catalogRequest=nil if reply.ok then local catalog,err=sanitiseRecipeCatalog(reply.result,query,pending.offset,now)state.recipeCatalog=catalog or{query=query,offset=pending.offset or offset,rows={},total=0,error=err,updated=now}if catalog then crafting.clearRecoveredRecipeError(state)end else state.recipeCatalog={query=query,offset=pending.offset or offset,rows={},total=0,error=tostring(reply.result or\"Recipe Server rejected catalogue request\"),updated=now}end elseif now-(pending.sent or now)>REMOTE_READ_TIMEOUT then local attempt=math.max(1,math.floor(tonumber(pending.attempt)or 1))state.catalogRequest=nil if attempt<3 then state.recipeCatalog.pending,state.recipeCatalog.retrying,state.recipeCatalog.error=true,attempt,nil state.nextCatalogRefresh=now+(250*(2^(attempt-1)))else state.recipeCatalog={query=query,offset=pending.offset or offset,rows={},total=0,error=\"Recipe Server did not respond to catalogue request\",updated=now}end end return end if now<(state.nextCatalogRefresh or 0)then return end local requestId,err=requestPeerCraft(source,\"recipe_catalog_request\",{query=query,limit=48,offset=offset})if requestId then state.catalogRequest,state.nextCatalogRefresh={id=requestId,sent=now,query=query,offset=offset,attempt=math.max(1,math.floor(tonumber(current.retrying)or 0)+1)},now+15000 state.recipeCatalog.pending=true else state.recipeCatalog={query=query,offset=offset,rows={},total=0,error=tostring(err or\"could not reach Recipe Server\"),updated=now}end return end if recipes then if now<(state.nextCatalogRefresh or 0)then return end local rows={}for name,recipe in pairs(recipes)do rows[#rows+1]={name=name,tags=crafting.recipeTagReferences(recipe)}end table.sort(rows,function(a,b)return a.name<b.name end)local matching={}local needle=query:lower()for _,row in ipairs(rows)do local short=crafting.displayName(row.name):lower()if needle==\"\"or row.name:lower():find(needle,1,true)or short:find(needle,1,true)then matching[#matching+1]=row end if#matching>=64 then break end end state.recipeCatalog,state.nextCatalogRefresh={query=query,offset=0,rows=matching,total=#rows,truncated=#matching<#rows,updated=now},now+15000 elseif not source then state.recipeCatalog={query=query,offset=0,rows={},total=0,error=\"Select a Recipe Server\",updated=now}end end local function sanitiseTagOptions(result,item,now)if type(result)~=\"table\"or result.item~=item or type(result.tags)~=\"table\"then return nil,\"tag options were malformed\"end local tags={}for index,tag in ipairs(result.tags)do if index>16 or type(tag)~=\"table\"or type(tag.tag)~=\"string\"or not tag.tag:match(\"^#[%w_.:%-]+$\")or type(tag.options)~=\"table\"then return nil,\"tag options contained invalid data\"end local options={}for optionIndex,row in ipairs(tag.options)do if optionIndex>48 or type(row)~=\"table\"or type(row.name)~=\"string\"or not row.name:match(\"^[%w_.:%-]+$\")then return nil,\"tag options contained an invalid item\"end options[#options+1]={name=row.name,count=math.max(0,math.floor(tonumber(row.count)or 0)),craftable=row.craftable==true}end if#options==0 then return nil,\"tag has no concrete options\"end tags[#tags+1]={tag=tag.tag,options=options,default=type(tag.default)==\"string\"and tag.default or options[1].name}end return{item=item,tags=tags,pending=nil,updated=now}end local function refreshRecipeTagOptions(state,source,recipes,tags,now)local pending=type(state.tagOptions)==\"table\"and state.tagOptions or nil if not pending or type(pending.item)~=\"string\"or pending.ready then return end if source and source.transport==\"peer\"then local request=state.tagOptionsRequest if request then local reply=takeCraftReply(request.id)if reply then state.tagOptionsRequest=nil if reply.ok and type(reply.result)==\"table\"and reply.result.item==pending.item and type(reply.result.tags)==\"table\"then local clean,cleanErr=sanitiseTagOptions(reply.result,pending.item,now)state.tagOptions=clean or{item=pending.item,pending=nil,error=cleanErr,updated=now}else pending.pending,pending.error,pending.updated=nil,tostring(reply.result or\"Recipe Server rejected tag options\"),now end elseif now-(request.sent or now)>REMOTE_READ_TIMEOUT then state.tagOptionsRequest,pending.pending,pending.error,pending.updated=nil,nil,\"Recipe Server did not respond to tag options\",now end return end local requestId,err=requestPeerCraft(source,\"recipe_tag_options_request\",{item=pending.item,stock=crafting.recipeStockSnapshot(state.stock)})if requestId then state.tagOptionsRequest={id=requestId,sent=now};pending.pending=true else pending.pending,pending.error=nil,tostring(err or\"could not reach Recipe Server\")end elseif recipes then local result,err=crafting.recipeTagOptions(recipes,tags,pending.item,state.stock)pending.tags,pending.pending,pending.error,pending.updated=result and result.tags or nil,nil,err,now else pending.pending,pending.error,pending.updated=nil,\"Recipe Server is unavailable\",now end end local function validCraftCommandToken(value)return type(value)==\"string\"and#value>=8 and#value<=80 and value:match(\"^[%w%._%-]+$\")~=nil end local function craftCommandResultPath(token)return validCraftCommandToken(token)and fs.combine(CRAFT_COMMAND_RESULTS,token..\".lua\")or nil end finaliseCraftCommands=function(completed,state)for _,item in ipairs(type(completed)==\"table\"and completed.files or{})do local acknowledgement=(state.commandLedger and state.commandLedger[item.token])or(state.acknowledgements and state.acknowledgements[item.token])local receipt=craftCommandResultPath(item.token)if acknowledgement and receipt then local saved=store.write(receipt,{ok=acknowledgement.ok==true,error=acknowledgement.error,at=acknowledgement.at})if saved then fs.delete(item.path)if not fs.exists(item.path)then if state.commandLedger then state.commandLedger[item.token]=nil end if state.acknowledgements then state.acknowledgements[item.token]=nil end end end end end if completed and completed.legacy then store.write(CRAFT_COMMAND_LEGACY,{})end if fs.exists(CRAFT_COMMAND_RESULTS)and fs.isDir(CRAFT_COMMAND_RESULTS)then local names=fs.list(CRAFT_COMMAND_RESULTS);table.sort(names)while#names>64 do fs.delete(fs.combine(CRAFT_COMMAND_RESULTS,table.remove(names,1)))end end end local function consumeCraftCommands(state)local completed={files={},legacy=false}local processed,consumedBytes=0,0 local maxCommands,maxBytes=16,48*1024 state.acknowledgements=type(state.acknowledgements)==\"table\"and state.acknowledgements or{}state.commandLedger=type(state.commandLedger)==\"table\"and state.commandLedger or{}if auth.refreshSession then auth.refreshSession()end if fs.exists(CRAFT_COMMAND_LEGACY)then completed.legacy=true end if fs.exists(CRAFT_COMMANDS)and fs.isDir(CRAFT_COMMANDS)then local names=fs.list(CRAFT_COMMANDS);table.sort(names)for _,name in ipairs(names)do if processed>=maxCommands or consumedBytes>=maxBytes then break end local path=fs.combine(CRAFT_COMMANDS,name)local handle=fs.open(path,\"r\");local raw=handle and handle.readAll()or nil if handle then handle.close()end processed,consumedBytes=processed+1,consumedBytes+#(raw or\"\")local command=raw and textutils.unserialise(raw)or nil local token=type(command)==\"table\"and command.token or nil if type(command)~=\"table\"or not validCraftCommandToken(token)then fs.delete(path)elseif fs.exists(craftCommandResultPath(token))then fs.delete(path)elseif state.commandLedger[token]or state.acknowledgements[token]then completed.files[#completed.files+1]={path=path,token=token}elseif crafting.commandIsStale(command,CRAFT_COMMAND_BOOT_AT)then local acknowledgement={ok=false,error=\"stale Crafting command discarded after restart\",at=os.epoch(\"utc\")}state.acknowledgements[token],state.commandLedger[token]=acknowledgement,acknowledgement completed.files[#completed.files+1]={path=path,token=token}else applyCraftCommand(state,command)local acknowledgement=state.acknowledgements[token]if acknowledgement then state.commandLedger[token]=acknowledgement completed.files[#completed.files+1]={path=path,token=token}else fs.delete(path)end end end end return completed end local function tickCraftQueue()craftRuntime.stage,craftRuntime.at=\"load_state\",os.epoch(\"utc\")local state=craftState();local completedCommands=consumeCraftCommands(state)local now=os.epoch(\"utc\")if now>=(state.nextSourceRefresh or 0)then craftRuntime.stage,craftRuntime.at=\"refresh_sources\",now refreshCraftSources(state,true)state.nextSourceRefresh=now+8000 end local tickerSource,requesterSource,recipeServer=craftSources.kind(state.config.ticker),craftSources.kind(state.config.requester),state.config.recipeServer craftRuntime.stage,craftRuntime.at=\"load_recipes\",now local recipes,recipeTags,recipeError=craftRecipes()craftRuntime.stage,craftRuntime.at=\"refresh_recipe_catalog\",now refreshRecipeCatalog(state,recipeServer,recipes,now)refreshRecipeTagOptions(state,recipeServer,recipes,recipeTags,now)if recipeError and not recipeServer then state.error=recipeError end if not tickerSource then state.error=\"Select a Stock Ticker source\"saveCraft(state,completedCommands)return 0.75 end local refreshStock=crafting.shouldRefreshStock(state)if refreshStock and now>=(state.nextRefresh or 0)then craftRuntime.stage,craftRuntime.at=\"refresh_stock\",now state.stockRefreshRequested=false local ok,err=refreshCraftStock(state,tickerSource,now)if not ok and err then state.error=err end state.nextRefresh=now+(state.active and 500 or 5000)end if not state.active and#state.queue>0 then state.active=table.remove(state.queue,1);state.active.index,state.active.phase,state.active.step=1,\"prepare\",1 end local active=state.active if active then local item=active.items[active.index]local timeout=(tonumber(state.config.timeout)or 120)*1000 if active.deadline and now>active.deadline and active.phase:find(\"^await_\")then local waiting=(active.phase==\"await_crafted_output\"or active.phase==\"await_craft_batch_output\"or active.phase==\"await_craft_step_output\")and\"recipe output in Stock Ticker\"or active.phase==\"await_package_confirmation\"and\"Stock Ticker package confirmation\"or\"Stock Ticker delivery confirmation\"failCraft(state,\"timed out waiting for \"..waiting)saveCraft(state,completedCommands)return 0.1 end if not item then completeCraftItem(state)elseif active.phase==\"prepare\"then if active.craftMode then if recipeServer and recipeServer.transport==\"peer\"then if not active.planRequest then local requestId,err=requestPeerCraft(recipeServer,\"recipe_plan_request\",{item=item.name,quantity=item.count,stock=crafting.recipeStockSnapshot(state.stock),tagChoices=crafting.normaliseTagChoices(item.tagChoices)or{}})if not requestId then failCraft(state,err)else active.planRequest,active.phase={id=requestId,sent=now,source=recipeServer.peer},\"await_peer_plan\"end saveCraft(state,completedCommands);return 0.1 end elseif recipeServer and recipeServer.transport==\"cloud\"then local plan,planErr=cloudSync.recipePlan(item.name,item.count,state.stock,crafting.normaliseTagChoices(item.tagChoices)or{})if not plan then failCraft(state,planErr or\"cloud Recipe Server is unavailable\")else active.plan,active.step,active.phase=plan,1,\"craft\"end else local plan,planErr=recipes and crafting.plan(recipes,state.stock,item.name,item.count,{forceRoot=true,tags=recipeTags,tagChoices=crafting.normaliseTagChoices(item.tagChoices)or{}})or nil,recipeError if not plan then failCraft(state,planErr or\"recipe database is unavailable\")else active.plan,active.step,active.phase=plan,1,\"craft\"end end else active.phase=\"deliver\"end elseif active.phase==\"await_peer_plan\"then local pending=active.planRequest;local reply=takeCraftReply(pending and pending.id)if reply then active.planRequest=nil local plan,planErr=reply.ok and crafting.validateExecutionPlan(reply.result)or nil,(reply and reply.result)if not plan then failCraft(state,planErr or\"recipe database did not return a valid plan\")else active.plan,active.step,active.phase=plan,1,\"craft\"end elseif pending and now-(pending.sent or now)>REMOTE_READ_TIMEOUT then failCraft(state,\"recipe database did not respond\")end elseif active.phase==\"craft\"then local step=active.plan[active.step]if step then active.stepAddress,active.stepRoute=step.address,(step.dispatch==\"requester\"and\"Requester -> \"or\"Stock Ticker -> \")..tostring(step.address)active.packageCount=step.inputs and#step.inputs or nil end if not step then active.phase=\"deliver\"elseif step.dispatch==\"requester\"and(not requesterSource or not sourceAvailable(requesterSource,\"redstonerequester\"))then failCraft(state,\"Recipe \"..tostring(step.name)..\" needs an available Redstone Requester for Crafter\")elseif step.dispatch==\"package\"then if active.craftStep~=active.step then active.craftStep,active.packageIndex,active.craftBaseline=active.step,1,crafting.normalise(state.stock)[step.name]or 0 end local input=step.inputs and step.inputs[active.packageIndex]if not input then active.phase,active.baseline,active.waitingFor,active.expectedOutput,active.deadline=\"await_crafted_output\",active.craftBaseline,step.name,step.produced,now+timeout state.nextRefresh=0 elseif not active.packageRequest then local baseline=crafting.normalise(state.stock)[input.name]or 0 local requested=active.packageRemaining or input.count active.craftBatchBaseline=crafting.normalise(state.stock)[step.name]or 0 local ok,result if tickerSource.transport==\"peer\"then local requestId,err=requestPeerCraft(tickerSource,\"craft_request\",{name=tickerSource.name,destination=step.address,filters={{name=input.name,_requestCount=requested}}})if not requestId then failCraft(state,err)else active.packageRequest,active.phase={id=requestId,input=input,requested=requested,baseline=baseline,sent=now},\"await_peer_package\"end saveCraft(state,completedCommands);return 0.1 else ok,result=peripherals.craftRequest(\"local\",tickerSource.name,step.address,{{name=input.name,_requestCount=requested}})end local accepted,acceptErr=ok and crafting.acceptedCount(result,requested)or nil,result if not accepted then failCraft(state,acceptErr or\"Stock Ticker rejected package\")else active.issued,active.issuedKind,active.issuedAt=true,\"package\",now active.packageRequest={input=input,baseline=baseline,requested=requested,accepted=accepted,remaining=requested-accepted}active.phase,active.waitingFor,active.deadline=\"await_package_confirmation\",input.name,now+timeout state.nextRefresh=0 end end else if active.craftStep~=active.step then active.craftStep,active.pendingCraftRequests,active.craftIssued,active.craftBaseline,active.nextCraftDispatch,active.nextCraftCheckpoint=active.step,step.batches,0,crafting.normalise(state.stock)[step.name]or 0,0,0 end if now>=(active.nextCraftDispatch or 0)then local ok,result if requesterSource.transport==\"peer\"then if not active.peerRequest then local requestId,err=requestPeerCraft(requesterSource,\"craft_recipe_request\",{name=requesterSource.name,address=step.address,batches=1,grid=step.grid})if not requestId then failCraft(state,err)else active.peerRequest,active.phase={id=requestId,kind=\"craft\",sent=now},\"await_peer_craft\"end end saveCraft(state,completedCommands);return 0.1 else ok,result=peripherals.craftingRecipeRequest(\"local\",requesterSource.name,step.address,1,step.grid)end if not ok then failCraft(state,result)else active.issued,active.issuedKind,active.issuedAt=true,\"craft\",now active.requesterConfigured=type(result)==\"table\"and result.configured==true active.requesterStrict=type(result)==\"table\"and result.strict==true active.pendingCraftRequests=active.pendingCraftRequests-1 active.craftIssued=(active.craftIssued or 0)+1 active.lastCraftDispatch=now local progress=crafting.requesterStepProgress(step.batches,active.craftIssued,step.output)if not progress then failCraft(state,\"invalid requester dispatch progress\")elseif progress.remaining>0 then active.nextCraftDispatch=now+100 else active.phase,active.waitingFor,active.expectedOutput,active.deadline=\"await_craft_step_output\",step.name,progress.expectedOutput,now+timeout state.nextRefresh=0 end end end end elseif active.phase==\"await_peer_craft\"then local reply=takeCraftReply(active.peerRequest and active.peerRequest.id)if reply then active.peerRequest=nil if not reply.ok then failCraft(state,reply.result)else local step=active.plan[active.step]active.issued,active.issuedKind,active.issuedAt=true,\"craft\",now active.pendingCraftRequests=active.pendingCraftRequests-1 active.craftIssued=(active.craftIssued or 0)+1 active.lastCraftDispatch=now local progress=crafting.requesterStepProgress(step.batches,active.craftIssued,step.output)if not progress then failCraft(state,\"invalid requester dispatch progress\")elseif progress.remaining>0 then active.phase,active.nextCraftDispatch,active.deadline=\"craft\",now+100,nil else active.phase,active.waitingFor,active.expectedOutput,active.deadline=\"await_craft_step_output\",step.name,progress.expectedOutput,now+timeout state.nextRefresh=0 end end elseif active.peerRequest and now-(active.peerRequest.sent or now)>REMOTE_READ_TIMEOUT then failCraft(state,\"Redstone Requester did not respond\")end elseif active.phase==\"await_peer_package\"then local pending=active.packageRequest local reply=takeCraftReply(pending and pending.id)if reply then active.packageRequest=nil local accepted,err=reply.ok and crafting.acceptedCount(reply.result,pending.requested or pending.input.count)or nil,reply.result if not accepted then failCraft(state,err or\"peer Stock Ticker rejected package\")else active.issued,active.issuedKind,active.issuedAt=true,\"package\",now active.packageRequest={input=pending.input,baseline=pending.baseline,requested=pending.requested or pending.input.count,accepted=accepted,remaining=(pending.requested or pending.input.count)-accepted}active.phase,active.waitingFor,active.deadline=\"await_package_confirmation\",pending.input.name,now+timeout state.nextRefresh=0 end elseif pending and now-(pending.sent or now)>REMOTE_READ_TIMEOUT then failCraft(state,\"peer Stock Ticker did not respond to package request\")end elseif active.phase==\"await_package_confirmation\"then local package=active.packageRequest if not package then failCraft(state,\"lost package confirmation state\")elseif(crafting.normalise(state.stock)[package.input.name]or 0)<=package.baseline-package.accepted then if package.remaining and package.remaining>0 then active.packageRemaining,active.packageRequest,active.phase=package.remaining,nil,\"craft\"else active.packageIndex,active.packageRemaining,active.packageRequest,active.phase=(active.packageIndex or 1)+1,nil,nil,\"craft\"end else state.nextRefresh=0 end elseif active.phase==\"await_craft_batch_output\"then local step=active.plan[active.step]local observed=crafting.normalise(state.stock)[active.waitingFor]or 0 if crafting.confirmRequesterBatch(active.craftBatchBaseline,observed,step.output)then if(active.pendingCraftRequests or 0)>0 then active.phase,active.nextCraftDispatch,active.deadline=\"craft\",now+100,nil else active.step,active.phase=active.step+1,\"craft\"active.craftStep,active.craftBaseline,active.craftBatchBaseline,active.nextCraftDispatch,active.deadline=nil,nil,nil,nil,nil end else state.nextRefresh=0 end elseif active.phase==\"await_craft_step_output\"then local step=active.plan[active.step]local observed=crafting.normalise(state.stock)[active.waitingFor]or 0 if observed>=(active.craftBaseline or 0)+(active.expectedOutput or 0)then active.step,active.phase=active.step+1,\"craft\"active.craftStep,active.craftIssued,active.craftBaseline,active.craftBatchBaseline,active.nextCraftDispatch,active.deadline=nil,nil,nil,nil,nil,nil else state.nextRefresh=0 end elseif active.phase==\"await_crafted_output\"then local step=active.plan[active.step]if(crafting.normalise(state.stock)[active.waitingFor]or 0)>=active.baseline+step.produced then active.step=active.step+1;active.phase=\"craft\"else state.nextRefresh=0 end elseif active.phase==\"deliver\"then local baseline=crafting.normalise(state.stock)[item.name]or 0 local ok,result if tickerSource.transport==\"peer\"then if not active.peerRequest then local requestId,err=requestPeerCraft(tickerSource,\"craft_request\",{name=tickerSource.name,destination=active.destination,filters={{name=item.name,_requestCount=item.count}}})if not requestId then failCraft(state,err)else active.peerRequest,active.phase={id=requestId,kind=\"delivery\",baseline=baseline,sent=now},\"await_peer_delivery\"end end saveCraft(state,completedCommands);return 0.1 else ok,result=peripherals.craftRequest(\"local\",tickerSource.name,active.destination,{{name=item.name,_requestCount=item.count}})end local accepted,acceptError if ok then accepted,acceptError=crafting.acceptedCount(result,item.count)else acceptError=result end if not accepted then failCraft(state,acceptError or result)else active.requested,active.accepted,active.delivered,active.remaining=item.count,accepted,0,item.count-accepted active.issued,active.issuedKind,active.issuedAt=true,\"delivery\",now active.phase,active.baseline,active.waitingFor,active.deadline=\"await_delivery_confirmation\",baseline,item.name,now+timeout state.nextRefresh=0 end elseif active.phase==\"await_peer_delivery\"then local reply=takeCraftReply(active.peerRequest and active.peerRequest.id)if reply then local baseline=active.peerRequest.baseline;active.peerRequest=nil local accepted,err=reply.ok and crafting.acceptedCount(reply.result,item.count)or nil,reply.result if not accepted then failCraft(state,err or\"peer Stock Ticker rejected the request\")else active.requested,active.accepted,active.delivered,active.remaining=item.count,accepted,0,item.count-accepted active.issued,active.issuedKind,active.issuedAt=true,\"delivery\",now active.phase,active.baseline,active.waitingFor,active.deadline=\"await_delivery_confirmation\",baseline,item.name,now+timeout;state.nextRefresh=0 end end elseif active.phase==\"await_delivery_confirmation\"then if(crafting.normalise(state.stock)[active.waitingFor]or 0)<=active.baseline-active.accepted then active.delivered=active.accepted local itemResult={id=active.id,status=active.remaining>0 and\"partial-dispatched\"or\"dispatched\",name=item.name,requested=active.requested,accepted=active.accepted,dispatched=active.delivered,remaining=active.remaining,destination=active.destination,finished=os.epoch(\"utc\")}pushCraftHistory(state,itemResult)if active.remaining>0 then state.sequence=state.sequence+1 state.retries[#state.retries+1]={id=\"craft-retry-\"..tostring(os.epoch(\"utc\"))..\"-\"..tostring(state.sequence),items={{name=item.name,count=active.remaining}},craftMode=active.craftMode,destination=active.destination,retryOf=active.id}end local completedJob=active.index>=#(active.items or{})completeCraftItem(state)if completedJob then state.error,state.recoveryNotice=nil,nil end else state.nextRefresh=0 end end end craftRuntime.stage,craftRuntime.at=\"persist\",os.epoch(\"utc\")saveCraft(state,completedCommands)craftRuntime.stage,craftRuntime.at=\"idle\",os.epoch(\"utc\")return state.active and 0.1 or 0.5 end local function craftingStatus()local state=craftRuntimeState if type(state)~=\"table\"then store.invalidate(CRAFT_STATE)state=store.read(CRAFT_STATE,craftDefault())end local active,item=state.active,state.active and state.active.items and state.active.items[state.active.index]local history=state.history or{}local stock,stockRows=crafting.normalise(state.stock),{}local function sourceStatus(value)value=craftSources.kind(value)if not value then return nil end return{transport=tostring(value.transport or\"local\"),name=value.name and tostring(value.name)or nil,peer=value.peer and tostring(value.peer)or nil,label=value.label and tostring(value.label)or nil}end for name,count in pairs(stock)do stockRows[#stockRows+1]={name=name,count=count}end table.sort(stockRows,function(a,b)return a.name<b.name end)while#stockRows>64 do table.remove(stockRows)end local catalog=type(state.recipeCatalog)==\"table\"and state.recipeCatalog or{}return{active=active and{id=active.id,phase=active.phase,name=item and item.name,requested=active.requested or(item and item.count),accepted=active.accepted,dispatched=active.delivered,remaining=active.remaining,destination=active.destination,issued=active.issued,issuedKind=active.issuedKind,requesterConfigured=active.requesterConfigured,requesterStrict=active.requesterStrict,waitingFor=active.waitingFor,expectedOutput=active.expectedOutput,requesterBatchBaseline=active.craftBatchBaseline,observedOutput=active.waitingFor and(stock[active.waitingFor]or 0)or nil,pendingCraftRequests=active.pendingCraftRequests,craftIssued=active.craftIssued,lastCraftDispatch=active.lastCraftDispatch,stepAddress=active.stepAddress,stepRoute=active.stepRoute,packageIndex=active.packageIndex,packageCount=active.packageCount}or nil,queue=#(state.queue or{}),retries=#(state.retries or{}),craftMode=state.craftMode==true,error=state.error,recoveryNotice=state.recoveryNotice,recipeServer=sourceStatus(state.config and state.config.recipeServer),authorization=auth.sessionStatus and auth.sessionStatus()or{operator=auth.allowed(\"operator\")},last=history[#history],stockUpdated=state.stockUpdated,recipeCatalog={query=tostring(catalog.query or\"\"),rows=#(catalog.rows or{}),total=math.max(0,math.floor(tonumber(catalog.total)or 0)),pending=catalog.pending==true,error=catalog.error and tostring(catalog.error):sub(1,180)or nil,updated=catalog.updated},stock=stockRows,config={ticker=sourceStatus(state.config and state.config.ticker),requester=sourceStatus(state.config and state.config.requester),recipeServer=sourceStatus(state.config and state.config.recipeServer),orderAddress=state.config and state.config.orderAddress and tostring(state.config.orderAddress)or\"Order\"},runtime={state=tostring(craftRuntime.state or\"unknown\"),stage=tostring(craftRuntime.stage or\"unknown\"),tick=math.max(0,math.floor(tonumber(craftRuntime.tick)or 0)),started=tonumber(craftRuntime.started),at=tonumber(craftRuntime.at),finished=tonumber(craftRuntime.finished),error=craftRuntime.error and tostring(craftRuntime.error):sub(1,180)or nil,persistenceError=craftRuntime.persistenceError,persistence=craftRuntime.persistence and{attempts=math.max(0,math.floor(tonumber(craftRuntime.persistence.attempts)or 0)),action=tostring(craftRuntime.persistence.action or\"unknown\"),ok=craftRuntime.persistence.ok==true,error=craftRuntime.persistence.error and tostring(craftRuntime.persistence.error):sub(1,180)or nil,at=tonumber(craftRuntime.persistence.at),writtenAt=tonumber(craftRuntime.persistence.writtenAt)}or nil},}end local function queueDevCraftE2E(state,items,destination)local connection,lease=readDevBuildRecord(\"/ceetos-dev/connection.lua\"),readDevBuildRecord(\"/ceetos-dev/recovery/developer-mode.lua\")if not devbridgePolicy.allowsUnattendedCraftProbe(connection,lease,profile.id(),os.epoch(\"utc\"))then auth.require(\"operator\")end if state.active or#(state.queue or{})>0 or#(state.cart or{})>0 then return false,\"Crafting Queue must be idle with an empty request list\"end if not sourceAvailable(state.config.ticker,\"stockticker\")then return false,\"selected Stock Ticker is unavailable\"end if not sourceAvailable(state.config.requester,\"redstonerequester\")then return false,\"selected Redstone Requester is unavailable\"end if not craftSources.kind(state.config.recipeServer)then return false,\"selected Recipe Server is unavailable\"end if type(items)~=\"table\"or#items<1 or#items>16 then return false,\"invalid craft probe items\"end if type(destination)~=\"string\"or#destination<1 or#destination>64 or destination:find(\"[%z\\1-\\31\\127]\")then return false,\"invalid craft probe destination\"end local prefix=\"dev-e2e-\"..tostring(os.epoch(\"utc\"))..\"-\"..tostring(math.random(100000,999999))if not fs.exists(CRAFT_COMMANDS)then fs.makeDir(CRAFT_COMMANDS)end local commands={{action=\"mode\",value=true},{action=\"destination\",value=destination}}for _,item in ipairs(items)do if type(item)~=\"table\"or type(item.name)~=\"string\"or not item.name:match(\"^[%w_.%-]+:[%w_./%-]+$\")or#item.name>128 then return false,\"invalid craft probe item\"end local count=tonumber(item.count)if not count or count<1 or count>256 or count~=math.floor(count)then return false,\"invalid craft probe quantity\"end commands[#commands+1]={action=\"add\",name=item.name,count=count}end commands[#commands+1]={action=\"submit\"}for index,command in ipairs(commands)do command.token=prefix..\"-\"..string.format(\"%02d\",index)command.createdAt=os.epoch(\"utc\")command.devProbe=true local temporary=fs.combine(CRAFT_COMMANDS,command.token..\".tmp\")local path=fs.combine(CRAFT_COMMANDS,command.token..\".lua\")local handle=fs.open(temporary,\"w\")if not handle then return false,\"could not stage craft probe command\"end handle.write(textutils.serialise(command));handle.close()if fs.exists(path)then fs.delete(path)end fs.move(temporary,path)end if os.queueEvent then pcall(os.queueEvent,CRAFT_COMMAND_EVENT,prefix)end return true,{queued=#items,destination=destination,tokenPrefix=prefix}end local function craftingPeripheralStatus()local state=craftState()refreshCraftSources(state,false)local tickerSource,requesterSource=craftSources.kind(state.config.ticker),craftSources.kind(state.config.requester)local requester=requesterSource and requesterSource.transport~=\"peer\"and peripheral.wrap(requesterSource.name)or nil local encoded=nil if requester and type(requester.getRequest)==\"function\"then local ok,value=pcall(requester.getRequest)if ok and type(value)==\"table\"then for _ in pairs(value)do encoded=(encoded or 0)+1 end end end local configuration=requester and type(requester.getConfiguration)==\"function\"and select(2,pcall(requester.getConfiguration))or nil local address=requester and type(requester.getAddress)==\"function\"and select(2,pcall(requester.getAddress))or nil return{ticker=tickerSource and{name=tickerSource.name,transport=tickerSource.transport,peer=tickerSource.peer,available=sourceAvailable(tickerSource,\"stockticker\")}or nil,requester=requesterSource and{name=requesterSource.name,transport=requesterSource.transport,peer=requesterSource.peer,available=sourceAvailable(requesterSource,\"redstonerequester\"),configuration=configuration,address=address,encodedSlots=encoded}or nil,sources=state.sources}end local function processDevNetworkCommand()local command=networkIpc.take()if type(command)~=\"table\"then return end writeDevValue(DEV_NETWORK_IPC_DIAG,{id=command.id,action=command.action,phase=\"received\",at=os.epoch(\"utc\")})local ok,result,detail=pcall(function()if command.action==\"network_status\"then return net.statusSnapshot(true)elseif command.action==\"route_status\"then return net.routeStatus(command.target)elseif command.action==\"network_trace\"then return net.transportTrace(command.kind)elseif command.action==\"peripheral_status\"then return{localDevices=peripherals.listLocal(),sharedDevices=peripherals.describe(),crafting=craftingPeripheralStatus()}elseif command.action==\"craft_status\"then return craftingStatus()elseif command.action==\"craft_e2e\"then return queueDevCraftE2E(craftState(),command.items,command.destination or\"Order\")elseif command.action==\"update_status\"then auth.require(\"operator\");return updater.status()elseif command.action==\"update_check\"then auth.require(\"operator\");return updater.check()elseif command.action==\"update_offers\"then auth.require(\"operator\");return updater.status().offers elseif command.action==\"update_install_latest\"then auth.require(\"operator\");return updater.installLatest()elseif command.action==\"update_download\"then auth.require(\"operator\");return updater.download(command.source)elseif command.action==\"update_cancel\"then auth.require(\"operator\");return updater.cancel()elseif command.action==\"update_clear_cache\"then auth.require(\"operator\");return updater.clearCache()elseif command.action==\"update_apply\"then auth.require(\"operator\")local applied,detail=updater.prepareApply()if not applied then return false,detail end return{reboot=true,detail=detail}elseif command.action==\"broker_status\"then auth.require(\"operator\");return releaseBroker.status()elseif command.action==\"broker_promote\"then auth.require(\"admin\");return releaseBroker.promote(command.ownerKey)elseif command.action==\"broker_revoke\"then auth.require(\"admin\");return releaseBroker.revoke(command.ownerKey)elseif command.action==\"broker_approve\"then auth.require(\"admin\");return releaseBroker.approve(command.session,command.phrase)elseif command.action==\"discovery_start\"then auth.require(\"operator\");return net.startDiscovery(command.mode or\"peer\",command.master or\"initiator\",command.phrase)elseif command.action==\"discovery_join\"then auth.require(\"operator\");return net.joinDiscovery(command.phrase)elseif command.action==\"discovery_confirm\"then auth.require(\"operator\");return net.confirmDiscovery()elseif command.action==\"discovery_cancel\"then auth.require(\"operator\")return net.cancelDiscovery(command.control,command.source or\"ipc\")elseif command.action==\"remote_request\"then auth.require(\"operator\");return net.request(command.target,command.type,command.body or{})elseif command.action==\"auth_send\"then if type(command.target)~=\"string\"or type(command.type)~=\"string\"or type(command.body)~=\"table\"then return false,\"invalid authority request\"end local directCheck=net.isTrustedDirectLink or net.isDirectPeer local trusted=directCheck and directCheck(command.target)==true if not authTransportPolicy.canSend(command.type,trusted)then return false,authTransportPolicy.denial(command.type)end return net.request(command.target,command.type,command.body,{safe=true,confidential=true})end error(\"unknown development network command\")end)if not ok then detail,result=result,nil elseif result==true and detail~=nil then result,detail=detail,nil elseif result==false then ok,detail=false,detail or\"network command failed\"end local replyError=nil if not ok then replyError=tostring(detail or\"network command failed\")end local replied,replyErr=networkIpc.reply(command.id,{id=command.id,ok=ok,result=ok and result or nil,error=replyError})if not replied then error(\"could not write network IPC result: \"..tostring(replyErr),0)end writeDevValue(DEV_NETWORK_IPC_DIAG,{id=command.id,action=command.action,phase=\"replied\",ok=ok,at=os.epoch(\"utc\")})networkIpc.prune(32)writeDevValue(DEV_NETWORK_STATUS,net.statusSnapshot(true))end math.randomseed(os.epoch(\"utc\"))auth.bootstrap()if authAuthorityRuntime then authAuthorityRuntime.start(authAuthority)end net.start()updater.recover()shell.setPath(\"/ceetos/bin:\"..shell.path())shell.setAlias(\"craftos-programs\",\"/rom/programs/programs.lua\")shell.setAlias(\"craftos-list\",\"/rom/programs/list.lua\")shell.setAlias(\"craftos-delete\",\"/rom/programs/delete.lua\")shell.setAlias(\"craftos-edit\",\"/rom/programs/edit.lua\")shell.setAlias(\"craftos-shell\",\"/ceetos/bin/craftos-shell.lua\")shell.setAlias(\"devices\",\"/ceetos/bin/ceetdevices.lua\")shell.setAlias(\"shareall\",\"/ceetos/bin/ceetshareall.lua\")shell.setAlias(\"unshareall\",\"/ceetos/bin/ceetunshareall.lua\")shell.setAlias(\"craft\",\"/ceetos/bin/ceetcraft.lua\")local function commandArgs(command)local result={}for token in tostring(command or\"\"):gmatch(\"%S+\")do result[#result+1]=token end return result end local function runCaptured(command)local output,old={},term.current()local capture={}function capture.write(text)output[#output+1]=tostring(text or\"\")end function capture.blit(text)output[#output+1]=tostring(text or\"\")end function capture.setCursorPos()end function capture.setCursorBlink()end setmetatable(capture,{__index=old})term.redirect(capture)local args=commandArgs(command)local ok,result=pcall(function()return shell.run(args[1],table.unpack(args,2))end)term.redirect(old)return ok,result,table.concat(output)end local function jobStatus()local state=jobs.state()local running=0;for _ in pairs(state.running or{})do running=running+1 end return{label=os.getComputerLabel(),capacity=state.capacity or 1,inFlight=running,queueDepth=#(state.queue or{}),available=true}end local function writeJobInbox(job)local inbox=store.read(JOB_INBOX,{})inbox[#inbox+1]=job while#inbox>16 do table.remove(inbox,1)end local ok,err=store.write(JOB_INBOX,inbox)if ok then pcall(os.queueEvent,JOB_EVENT)end return ok,err end local function processWorkerJob()store.invalidate(JOB_INBOX)local inbox=store.read(JOB_INBOX,{})local job=table.remove(inbox,1)if job then store.write(JOB_INBOX,inbox)local template=jobs.listTemplates()[job.template]local valid=template and template.revision==job.revision and template.command==job.command local ok,result,output=false,\"template mismatch\",\"\"if valid then ok,result,output=runCaptured(job.command)end local jobOk=valid and ok and result~=false local jobError=nil if not jobOk then jobError=tostring(result or\"template mismatch\")end local outbox=store.read(JOB_OUTBOX,{})outbox[#outbox+1]={id=job.id,controller=job.controller,ok=jobOk,output=output,error=jobError,}while#outbox>16 do table.remove(outbox,1)end store.write(JOB_OUTBOX,outbox)return true end return false end local function workerJobLoop()local timer=os.startTimer(2)while true do if processWorkerJob()then timer=os.startTimer(2)else local event,id=os.pullEventRaw()if event==JOB_EVENT or(event==\"timer\"and id==timer)then timer=os.startTimer(2)end end end end local function chooseWorker()local choices={}for workerId,worker in pairs(jobs.workers())do if worker.healthy and(worker.slots or 0)>0 then choices[#choices+1]=worker end end table.sort(choices,function(a,b)local aLoad,bLoad=(a.inFlight or 0)/math.max(1,a.capacity or 1),(b.inFlight or 0)/math.max(1,b.capacity or 1)if aLoad~=bLoad then return aLoad<bLoad end if(a.hops or 0)~=(b.hops or 0)then return(a.hops or 0)<(b.hops or 0)end return tostring(a.id)<tostring(b.id)end)return choices[1]end local function syncJobTemplates()if tostring(net.controller())~=tostring(os.getComputerID())then return end local payload={controller=net.controller(),templates=jobs.listTemplates()}for _,peer in ipairs(net.peers())do net.request(peer.id,\"job_sync\",payload)end end local function processJobOutbox()store.invalidate(JOB_OUTBOX)local outbox=store.read(JOB_OUTBOX,{})if#outbox==0 then return end store.write(JOB_OUTBOX,{})for _,row in ipairs(outbox)do if tostring(row.controller)==tostring(os.getComputerID())then if row.ok then pcall(jobs.complete,row.id,{output=row.output,ok=true})else pcall(jobs.fail,row.id,row.error or\"worker failed\")end else net.request(row.controller,\"job_result\",row)end end end local function dispatchJobs()if tostring(jobs.controller())~=tostring(net.controller())then jobs.setController(net.controller())end jobs.heartbeat(os.getComputerID(),jobStatus())if tostring(net.controller())~=tostring(os.getComputerID())then net.request(net.controller(),\"job_heartbeat\",jobStatus(),{safe=true})return end jobs.tick()if os.epoch(\"utc\")-lastJobTemplateSync>=10000 then syncJobTemplates();lastJobTemplateSync=os.epoch(\"utc\")end while true do local queued,worker=jobs.nextDispatch(),chooseWorker()if not queued or not worker then break end local ok,assigned=pcall(jobs.assign,queued,worker.id)if not ok then break end assigned.controller=tostring(os.getComputerID())if tostring(worker.id)==tostring(os.getComputerID())then writeJobInbox(assigned)else local sent,err=net.request(worker.id,\"job_dispatch\",assigned)if not sent then pcall(jobs.fail,assigned.id,err)end end end end local function syncAccounts()local started=os.epoch(\"utc\")local outcome=accountSync.broadcast(auth,net,function(progress)local total=math.max(1,tonumber(progress.total)or 0)writeDevValue(OPERATION_STATUS,{active=progress.phase~=\"complete\",name=\"Account sync\",progress=math.max(0,math.min(1,(tonumber(progress.completed)or 0)/total)),completed=progress.completed or 0,total=progress.total or 0,expires=started+1500,updated=os.epoch(\"utc\"),})end)if outcome and outcome.total and outcome.total>0 then writeDevValue(OPERATION_STATUS,{active=false,name=\"Account sync\",progress=1,completed=outcome.total,total=outcome.total,expires=started+1500,updated=os.epoch(\"utc\"),})end end net.publicHandler=function(packet,distance)local ok,err=pcall(function()if tostring(packet.type or\"\"):match(\"^broker_\")then return releaseBroker.handle(packet,distance)end return updater.publicPacket(packet,distance)end)if not ok then audit.log(\"system\",\"update.public_packet_error\",{error=tostring(err):sub(1,120)})end end net.handler=function(from,peer,packet)if authAuthorityRuntime and authAuthorityRuntime.handles(packet)then local handled,authorityErr=authAuthorityRuntime.handle(authAuthority,from,peer,packet,net)if handled==false then audit.log(\"system\",\"auth.authority_request_rejected\",{type=tostring(packet.type):sub(1,48),error=tostring(authorityErr or\"authority rejected request\"):sub(1,120)})end return handled,authorityErr end if packet.type==\"update_query\"or packet.type==\"update_offer\"or packet.type==\"update_chunk_request\"or packet.type==\"update_chunk\"then local ok,err=pcall(updater.authenticatedPacket,from,peer,packet)if not ok then audit.log(\"system\",\"update.packet_error\",{error=tostring(err):sub(1,120)})end return end if packet.type==\"announce\"then print(\"[peer] \"..from..\" is online\"..((packet.body and packet.body.label)and(\" (\"..packet.body.label..\")\")or\"\"))elseif packet.type==\"profile_query\"then net.reply(packet,\"profile_status\",{profile=profile.id(),version=version.string},{safe=true})elseif packet.type==\"profile_status\"then recordProfile(packet.origin or from,packet.body)elseif packet.type==\"peripheral_list\"then net.reply(packet,\"peripheral_list_result\",peripherals.describe(),{safe=true})elseif packet.type==\"peripheral_list_result\"then local origin=packet.origin or from local cache=store.read(\"/ceetos/data/remote/\"..origin..\".lua\",{})cache.peripherals,cache.updated=packet.body or{},os.epoch(\"utc\")store.write(\"/ceetos/data/remote/\"..origin..\".lua\",cache)pcall(os.queueEvent,\"ceetos_state_changed\",\"remote\",cache.updated)elseif packet.type==\"peripheral_call\"then local body=packet.body or{}local ok,result=peripherals.call(peer.master,body.name,body.method,body.args)net.reply(packet,\"peripheral_result\",{ok=ok,result=result})elseif packet.type==\"peripheral_result\"then local origin=packet.origin or from local cache=store.read(\"/ceetos/data/remote/\"..origin..\".lua\",{})cache.last_result,cache.updated=packet.body,os.epoch(\"utc\")store.write(\"/ceetos/data/remote/\"..origin..\".lua\",cache)pcall(os.queueEvent,\"ceetos_state_changed\",\"remote\",cache.updated)elseif packet.type==\"craft_stock_request\"then local body=packet.body or{}local origin=tostring(packet.origin or from)local ok,result=peripherals.call(peer.master,body.name,\"stock\",{true})net.reply(packet,\"craft_stock_result\",{request_id=body.request_id,ok=ok,result=result},{safe=true})elseif packet.type==\"craft_stock_result\"then local body=packet.body or{}recordCraftReply(body,packet.origin or from)elseif packet.type==\"recipe_plan_request\"then local body=packet.body or{}local valid=type(body.request_id)==\"string\"and#body.request_id<=120 and type(body.item)==\"string\"and#body.item<=128 and tonumber(body.quantity)and tonumber(body.quantity)>=1 and tonumber(body.quantity)<=256 and type(body.stock)==\"table\"local stockCount=0 for _,row in pairs(type(body.stock)==\"table\"and body.stock or{})do stockCount=stockCount+1 if stockCount>512 or type(row)~=\"table\"or type(row.name or row.id)~=\"string\"or tonumber(row.count or row.amount or row.quantity or 0)==nil then valid=false;break end end local ok,result=false,\"invalid recipe plan request\"if valid then local recipes,tags,err=craftRecipes()if recipes then local plan,planErr=crafting.plan(recipes,body.stock,body.item,math.floor(tonumber(body.quantity)),{forceRoot=true,tags=tags,tagChoices=body.tagChoices})if plan and#plan<=96 then local encoded=textutils.serialise(plan)if#encoded<=12*1024 then ok,result=true,plan else result=\"recipe plan exceeds remote reply limit\"end else result=planErr or\"recipe plan exceeds remote reply limit\"end else result=err or\"recipe database is unavailable\"end end net.reply(packet,\"recipe_plan_result\",{request_id=body.request_id,ok=ok,result=result},{safe=true})elseif packet.type==\"recipe_plan_result\"or packet.type==\"recipe_catalog_result\"or packet.type==\"recipe_tag_options_result\"then local body=packet.body or{}recordCraftReply(body,packet.origin or from)elseif packet.type==\"craft_request\"then local body=packet.body or{}local origin=tostring(packet.origin or from)local allowed=net.isDirectController(from,packet)and(peer.role==\"admin\"or peer.role==\"operator\")local ok,result=false,\"craft request is not authorised\"if allowed then ok,result=peripherals.craftRequest(peer.master,body.name,body.destination,body.filters)end net.reply(packet,\"craft_result\",{request_id=body.request_id,ok=ok,result=result})elseif packet.type==\"craft_recipe_request\"then local body=packet.body or{}local origin=tostring(packet.origin or from)local allowed=net.isDirectController(from,packet)and(peer.role==\"admin\"or peer.role==\"operator\")local ok,result=false,\"craft recipe request is not authorised\"if allowed then ok,result=peripherals.craftingRecipeRequest(peer.master,body.name,body.address,body.batches,body.grid)end net.reply(packet,\"craft_result\",{request_id=body.request_id,ok=ok,result=result})elseif packet.type==\"craft_result\"then local body=packet.body or{}recordCraftReply(body,packet.origin or from)elseif packet.type==\"shell_request\"then if peer.role~=\"admin\"and peer.role~=\"operator\"then return end local command=packet.body and packet.body.command if type(command)~=\"string\"or#command>240 or command:match(\"^%s*nx%s+recover\")then return end audit.log(\"peer:\"..from,\"shell.request\",{command=command,session=packet.body and packet.body.session})print(\"[remote \"..from..\"] \"..command)local ok,result,output=runCaptured(command)if packet.body and packet.body.session then local shellOk=ok and result~=false local shellError=nil if not shellOk then shellError=tostring(result)end net.reply(packet,\"shell_result\",{session=packet.body.session,ok=shellOk,output=output,error=shellError})end elseif packet.type==\"shell_result\"then local replies=store.read(SHELL_REPLY_PATH,{})replies[#replies+1]={from=from,body=packet.body or{},received=os.epoch(\"utc\")}while#replies>20 do table.remove(replies,1)end store.write(SHELL_REPLY_PATH,replies)elseif packet.type==\"auth_seed_request\"then return false,\"auth seed replication requires a v2 capability\"elseif packet.type==\"auth_authority\"then local directCheck=net.isTrustedDirectLink or net.isDirectPeer local ok,err=pcall(auth.acceptAuthority,packet.body,{from=from,direct=directCheck and directCheck(from)==true})if not ok then audit.log(\"system\",\"auth.authority_rejected\",{error=tostring(err):sub(1,120)})end elseif packet.type==\"auth_login_begin_result\"or packet.type==\"auth_login_proof_result\"or packet.type==\"auth_mutate_result\"then local directCheck=net.isTrustedDirectLink or net.isDirectPeer require((\"ceetos.lib.auth_client\")).receive(packet.body,{from=from,direct=directCheck and directCheck(from)==true,type=packet.type,routed=tostring(packet.origin or from)~=tostring(from),deferred=true})elseif packet.type==\"account_sync\"then local changed,mergeError=accountSync.receive(auth,packet)if changed then audit.log(\"peer:\"..from,\"accounts.sync\",{});syncAccounts()elseif mergeError then audit.log(\"peer:\"..from,\"accounts.sync_rejected\",{reason=tostring(mergeError):sub(1,48)})end elseif packet.type==\"job_heartbeat\"then if tostring(net.controller())==tostring(os.getComputerID())then local body=packet.body or{};body.hops=1 jobs.heartbeat(from,body)end elseif packet.type==\"job_sync\"then local body=packet.body or{}if net.isDirectController(from,packet)and tostring(body.controller)==tostring(net.controller())and type(jobs.syncTemplates)==\"function\"then jobs.syncTemplates(body.controller,body.templates or{})end elseif packet.type==\"job_dispatch\"then local body=packet.body or{}if not net.isDirectController(from,packet)or tostring(body.controller)~=tostring(net.controller())then audit.log(\"peer:\"..from,\"jobs.rejected\",{reason=\"not_controller\"});return end local template=jobs.listTemplates()[body.template]if not template or template.revision~=body.revision or template.command~=body.command then audit.log(\"peer:\"..from,\"jobs.rejected\",{reason=\"template_mismatch\"});return end writeJobInbox(body)elseif packet.type==\"job_result\"then if tostring(net.controller())==tostring(os.getComputerID())then local body=packet.body or{}if body.ok then pcall(jobs.complete,body.id,{output=body.output,ok=true})else pcall(jobs.fail,body.id,body.error or\"worker failed\")end end else return false,\"unsupported packet\"end return true end local candidateStartedAt=os.epoch(\"utc\")local recoveryHealthProof local candidateReadiness={network=false,crafting=false}local function runCraftingTick()craftRuntime.state,craftRuntime.stage,craftRuntime.tick,craftRuntime.started,craftRuntime.error=\"running\",\"starting_tick\",math.max(0,math.floor(tonumber(craftRuntime.tick)or 0))+1,os.epoch(\"utc\"),nil local ok,delayOrErr=pcall(tickCraftQueue)if not ok then craftRuntime.state,craftRuntime.stage,craftRuntime.error=\"error\",\"failed\",tostring(delayOrErr):sub(1,180)local state=craftState()failCraft(state,\"crafting queue service error: \"..tostring(delayOrErr))saveCraft(state)audit.log(\"system\",\"crafting.queue_error\",{error=tostring(delayOrErr)})else craftRuntime.state,craftRuntime.finished=\"running\",os.epoch(\"utc\")end if ok then candidateReadiness.crafting=true end if recoveryHealthProof then recoveryHealthProof()end local delay=ok and tonumber(delayOrErr)or nil return delay and math.max(0.05,math.min(delay,1))or 0.25 end local function craftingServiceLoop()craftingService.new({tick=runCraftingTick,startTimer=os.startTimer,pullEvent=os.pullEventRaw,now=function()return os.epoch(\"utc\")end,}):run()end local function cloudServiceLoop()cloudService.new({tick=cloudSync.tick,startTimer=os.startTimer,pullEvent=os.pullEventRaw,interval=5,}):run()end local function serviceLoop()local started,lastError=os.epoch(\"utc\"),nil local nextHealthAt=0 local function health(errorText,force)if errorText then lastError=tostring(errorText):sub(1,240)end local timestamp=os.epoch(\"utc\")if not force and timestamp<nextHealthAt then return end nextHealthAt=timestamp+2000 writeDevValue(DEV_NETWORK_HEALTH,{version=version.string,profile=profile.id(),started=started,lastEvent=os.epoch(\"utc\"),healthy=lastError==nil,error=lastError,ipc=networkIpc.status()})end health(nil,true)local runtime runtime=networkRuntime.new({now=function()return os.epoch(\"utc\")end,startTimer=os.startTimer,pullEvent=os.pullEventRaw,handle=net.handle,onFault=function(scope,errorText)health(errorText,true)sounds.play(\"error\")local names={ipc=\"network.ipc_error\",packet=\"network.packet_error\",event=\"network.event_error\"}audit.log(\"system\",names[scope]or(\"network.\"..tostring(scope)..\"_error\"),{error=tostring(errorText):sub(1,120)})end,ipc={event=networkIpc.EVENT,interval=0.5,cadence=250,pending=networkIpc.pending,claim=processDevNetworkCommand},onEvent=function(event)if event[1]==\"peripheral\"or event[1]==\"peripheral_detach\"then sounds.invalidate()sounds.refresh()end end,tasks={{name=\"discovery\",initial=0.25,interval=1,run=function()net.tick()pcall(os.queueEvent,craftingService.HEARTBEAT_EVENT)pcall(releaseBroker.tick)local snapshot=net.statusSnapshot(true)local wrote,writeErr=writeDevValue(DEV_NETWORK_STATUS,snapshot)if not wrote then error(writeErr or\"could not persist network snapshot\")end health()candidateReadiness.network=true if recoveryHealthProof then recoveryHealthProof()end return 1 end,},{name=\"accounts\",initial=2,interval=10,run=function()syncAccounts()if os.epoch(\"utc\")-lastProfileAdvert>=30000 then for _,peer in ipairs(net.peers())do net.request(peer.id,\"profile_status\",{profile=profile.id(),version=version.string},{safe=true})end lastProfileAdvert=os.epoch(\"utc\")end return 10 end,},{name=\"auth-authority\",initial=0.25,interval=1,run=function()if not authAuthority then return 30 end local ok,err=pcall(authAuthorityRuntime.tick,authAuthority,net)if not ok then audit.log(\"system\",\"auth.authority_service_error\",{error=tostring(err):sub(1,120)})return 2 end return 1 end,},{name=\"jobs\",initial=2,interval=2,run=function()processJobOutbox()local ok,err=pcall(dispatchJobs)if not ok then audit.log(\"system\",\"jobs.scheduler_error\",{error=tostring(err)})end return 2 end,},{name=\"updates\",initial=1,interval=1,run=function()local ok,err=pcall(updater.tick)if not ok then health(err,true);audit.log(\"system\",\"update.service_error\",{error=tostring(err):sub(1,120)})end return 1 end,},},})runtime:run()end local function userDesktop()print(\"CeetOS \"..version.string..\" | computer \"..os.getComputerID()..\" -- starting Desktop\")while true do if fs.exists(\"/ceetos/data/desktop-stop\")then fs.delete(\"/ceetos/data/desktop-stop\")end local ok=shell.run(\"/ceetos/bin/ceetui.lua\")if ok==false and not fs.exists(\"/ceetos/data/desktop-stop\")then printError(\"CeetUI stopped; restarting in 2 seconds...\")sleep(2)elseif fs.exists(\"/ceetos/data/desktop-stop\")then fs.delete(\"/ceetos/data/desktop-stop\")return end end end recoveryHealthProof=function()local path=\"/ceetos-dev/recovery/transaction.lua\"if not fs.exists(path)then return end local handle=fs.open(path,\"r\");local transaction=handle and textutils.unserialise(handle.readAll())or nil if handle then handle.close()end if type(transaction)~=\"table\"or transaction.state~=\"candidate\"or transaction.expectedVersion~=version.string then return end if not candidateReadiness.network or not candidateReadiness.crafting then return end local now=os.epoch(\"utc\")if type(transaction.candidateAt)~=\"number\"or candidateStartedAt<transaction.candidateAt then return end local proof={transaction=transaction.id,version=version.string,profile=profile.id(),startedAt=candidateStartedAt,runtimeReady=true,at=now,}local temporary,output=\"/ceetos-dev/recovery/candidate-health.lua.tmp\",fs.open(\"/ceetos-dev/recovery/candidate-health.lua.tmp\",\"w\")if output then output.write(textutils.serialise(proof));output.close();if fs.exists(\"/ceetos-dev/recovery/candidate-health.lua\")then fs.delete(\"/ceetos-dev/recovery/candidate-health.lua\")end;fs.move(temporary,\"/ceetos-dev/recovery/candidate-health.lua\");sounds.play(\"confirmation\")end end sounds.play(\"boot\")local function devBridgeLoop()local path=\"/ceetos-dev/connection.lua\"local restartPath=\"/ceetos-dev/recovery/restart-request.lua\"local heartbeatPath=\"/ceetos-dev/recovery/bridge-heartbeat.lua\"local function pendingRestartRequest()if not fs.exists(restartPath)then return nil end local handle=fs.open(restartPath,\"r\")local request=handle and textutils.unserialise(handle.readAll())or nil if handle then handle.close()end if type(request)~=\"table\"or request.schema~=1 then return nil end if request.reason~=\"candidate\"and request.reason~=\"restart\"and request.reason~=\"rollback\"and request.reason~=\"websocket-recovery\"and request.reason~=\"offline-repair\"then return nil end return request end local function runDevClient(cfg,token)local started=os.epoch(\"utc\")local function recoverBlockedClient()local temporary=restartPath..\".tmp\"if fs.exists(temporary)then pcall(fs.delete,temporary)end local output=fs.open(temporary,\"w\")if output then output.write(textutils.serialise({schema=1,reason=\"websocket-recovery\",requestedAt=os.epoch(\"utc\")}))output.close()if fs.exists(restartPath)then pcall(fs.delete,restartPath)end pcall(fs.move,temporary,restartPath)end os.reboot()end local function stalled()while true do sleep(2)local handle=fs.exists(heartbeatPath)and fs.open(heartbeatPath,\"r\")or nil local heartbeat=handle and textutils.unserialise(handle.readAll())or nil if handle then handle.close()end local at=type(heartbeat)==\"table\"and tonumber(heartbeat.at)or nil if os.epoch(\"utc\")-started>20000 and(not at or at<started or os.epoch(\"utc\")-at>15000)then recoverBlockedClient()end end end parallel.waitForAny(function()if token~=\"\"then shell.run(\"/ceetos/bin/ceetdev.lua\",cfg.url,token,\"--background\")else shell.run(\"/ceetos/bin/ceetdev.lua\",cfg.url,\"--background\")end end,stalled)end while true do if fs.exists(path)then local handle=fs.open(path,\"r\")local cfg=handle and textutils.unserialise(handle.readAll())or nil if handle then handle.close()end if type(cfg)==\"table\"and type(cfg.url)==\"string\"then local token=type(cfg.token)==\"string\"and cfg.token or\"\"local ran,result=pcall(runDevClient,cfg,token)if not ran or result==false then writeDevValue(\"/ceetos-dev/recovery/bridge-status.lua\",{schema=1,state=\"client-exited\",at=os.epoch(\"utc\"),detail=tostring(ran and\"ceetdev returned false\"or result):gsub(\"[%c]\",\" \"):sub(1,160),})end if pendingRestartRequest()then return end sleep(3)else sleep(5)end else sleep(5)end end end local function restartWatcher()local path=\"/ceetos-dev/recovery/restart-request.lua\"local function restartRequest()if not fs.exists(path)then return nil end local handle=fs.open(path,\"r\")local request=handle and textutils.unserialise(handle.readAll())or nil if handle then handle.close()end if type(request)==\"table\"and request.schema==1 and(request.reason==\"candidate\"or request.reason==\"restart\"or request.reason==\"rollback\"or request.reason==\"websocket-recovery\"or request.reason==\"offline-repair\")then return request end return nil end local timer=os.startTimer(2)local shouldCheck=true while true do if shouldCheck and restartRequest()then os.reboot()end local event,id=os.pullEventRaw()shouldCheck=event==\"ceetos_dev_restart\"or(event==\"timer\"and id==timer)if shouldCheck then timer=os.startTimer(2)end end end local serviceSupervisor=supervisor.new({now=function()return os.epoch(\"utc\")end,sleep=sleep,record=recordServiceTransition,baseDelay=0.25,maxDelay=5,})local function backgroundServices()parallel.waitForAll(function()serviceSupervisor:run(\"network\",serviceLoop)end,function()serviceSupervisor:run(\"jobs\",workerJobLoop)end,function()serviceSupervisor:run(\"crafting\",craftingServiceLoop)end,function()serviceSupervisor:run(\"cloud\",cloudServiceLoop)end,function()serviceSupervisor:run(\"devbridge\",devBridgeLoop)end)end parallel.waitForAny(userDesktop,backgroundServices,restartWatcher)",
  ["ceetos/bin/nx.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local auth=require(\"ceetos.lib.auth\")local net=require(\"ceetos.lib.net\")local peripherals=require(\"ceetos.lib.peripherals\")local telemetry=require(\"ceetos.lib.telemetry\")local cloudSync=require(\"ceetos.lib.cloud_sync\")local ownerService=require(\"ceetos.lib.owner_service\")local store=require(\"ceetos.lib.store\")local audit=require(\"ceetos.lib.audit\")local jobs=require(\"ceetos.lib.jobs\")local networkIpc=require(\"ceetos.lib.network_ipc\")local args={...}local requestSequence=0 local function networkRequest(action,body)requestSequence=requestSequence+1 local request=body or{}request.id=\"nx-\"..tostring(os.getComputerID())..\"-\"..tostring(os.epoch(\"utc\"))..\"-\"..tostring(requestSequence)request.action=action local submitted,submitErr=networkIpc.submit(request)if not submitted then return false,submitErr or\"CeetOS network service is unavailable\"end local response,waitErr=networkIpc.await(request.id,5000)if waitErr then return false,waitErr end return response and response.ok==true,response and(response.result or response.error)or\"CeetOS network service did not respond\"end local function usage()print(\"CeetOS: help, gui, craft, whoami, login, logout, users, user add/edit/delete/conflicts/conflict-dismiss, discover, network, peers, share, unshare, peripherals, remote, jobs, update, broker, telemetry, cloud status|enroll <code>|retry|clear, owner-service, recover\")end local function requireArgs(n)if#args<n then error(\"missing argument\",0)end end local command=args[1]or\"help\"if command==\"help\"then usage()elseif command==\"gui\"then shell.run(\"ceetui\")elseif command==\"craft\"then shell.run(\"ceetcraft\",table.unpack(args,2))elseif command==\"whoami\"then print(auth.current and(auth.current.name..\" (\"..auth.current.role..\")\")or\"not logged in\")elseif command==\"login\"then requireArgs(2);term.write(\"Password: \");local pass=read(\"*\")if auth.login(args[2],pass)then print(\"Logged in.\")else printError(\"Invalid credentials.\")end elseif command==\"logout\"then auth.logout();print(\"Logged out.\")elseif command==\"users\"then auth.require(\"operator\");for _,user in ipairs(auth.list())do print(user.name..\"\\t\"..user.role)end elseif command==\"user\"and args[2]==\"conflicts\"then auth.require(\"admin\")local rows=auth.conflicts and auth.conflicts()or{}if#rows==0 then print(\"No quarantined account conflicts.\")else for _,row in ipairs(rows)do print(row.name..\"\\tlocal \"..tostring(row.localRole)..\" @\"..tostring(row.localOrigin)..\"\\tremote \"..tostring(row.incomingRole)..\" @\"..tostring(row.incomingOrigin)..\"\\t\"..tostring(row.occurrences)..\" occurrence(s)\")end end elseif command==\"user\"and args[2]==\"conflict-dismiss\"then auth.require(\"admin\");requireArgs(3);assert(auth.dismissConflict(args[3]));print(\"Local account record retained; conflict dismissed.\")elseif command==\"user\"and args[2]==\"add\"then auth.require(\"operator\");requireArgs(4);term.write(\"Password for \"..args[3]..\": \");local pass=read(\"*\")auth.add(args[3],args[4],pass);audit.log(auth.current.name,\"user.add\",{name=args[3],role=args[4]});print(\"User created.\")elseif command==\"user\"and args[2]==\"edit\"then auth.require(\"operator\");requireArgs(4);auth.setRole(args[3],args[4]);audit.log(auth.current.name,\"user.role\",{name=args[3],role=args[4]});print(\"User role updated.\")elseif command==\"user\"and args[2]==\"delete\"then auth.require(\"operator\");requireArgs(3);auth.remove(args[3]);audit.log(auth.current.name,\"user.delete\",{name=args[3]});print(\"User deleted.\")elseif command==\"peers\"then local ok,result=networkRequest(\"network_status\",{})if not ok then printError(result)else for _,peer in ipairs(result.peers or{})do print(peer.id..\"\\t\"..peer.role..(peer.master and\"\\tmaster\"or\"\")..(peer.routed and\"\\trouted\"or\"\"))end end elseif command==\"discover\"then auth.require(\"operator\")if args[2]==\"status\"then local ok,result=networkRequest(\"network_status\",{});if not ok then printError(result)else print(textutils.serialize(result.discovery or{}))end elseif args[2]==\"cancel\"then local statusOk,status=networkRequest(\"network_status\",{})local control=statusOk and status and status.discovery and status.discovery.session and status.discovery.session.control if type(control)~=\"string\"or control==\"\"then printError(\"No active discovery session to cancel\")else local ok,result=networkRequest(\"discovery_cancel\",{control=control,source=\"nx\"});if ok then print(\"Discovery cancelled.\")else printError(result)end end elseif args[2]==\"join\"then local ok,result=networkRequest(\"discovery_join\",{phrase=args[3]})if not ok then printError(result)elseif type(result)==\"table\"and result.state==\"candidate\"then print(\"Matching computer found. Run 'nx discover confirm' to pair.\")else print(\"Searching for a matching discovery session.\")end elseif args[2]==\"confirm\"then local ok,err=networkRequest(\"discovery_confirm\",{});if not ok then printError(err)else print(\"Pairing confirmation sent.\")end elseif args[2]==\"start\"then requireArgs(3)local phrase=args[3];local mode=args[4]==\"master\"and\"master\"or\"peer\";local master=args[5]or\"initiator\"local ok,err=networkRequest(\"discovery_start\",{phrase=phrase,mode=mode,master=master})if not ok then printError(err)else print(\"Discovery started. Keep this phrase private and have the other computer enter it.\")end else printError(\"use discover start <phrase> [peer|master] [initiator|joiner], join <phrase>, confirm, cancel, or status\")end elseif command==\"network\"and args[2]==\"status\"then local ok,result=networkRequest(\"network_status\",{});if not ok then printError(result)else print(textutils.serialize(result.transport or{}))end elseif command==\"network\"and args[2]==\"routes\"then local ok,result=networkRequest(\"route_status\",{target=args[3]});if not ok then printError(result)else for _,route in ipairs(result or{})do print(tostring(route.id)..\" via \"..tostring(route.next)..\" / \"..tostring(route.hops)..\" hop [\"..tostring(route.health)..\"]\"..(route.reason and(\" - \"..tostring(route.reason))or\"\"))end end elseif command==\"network\"and args[2]==\"trace\"then local ok,result=networkRequest(\"network_trace\",{kind=args[3]});if not ok then printError(result)else print(textutils.serialize(result))end elseif command==\"update\"then auth.require(\"operator\")local sub=args[2]or\"status\"if sub==\"status\"then local ok,result=networkRequest(\"update_status\",{})if not ok then printError(result)else print(\"CeetOS \"..tostring(result.version)..\" | \"..tostring(result.status or\"idle\"))if result.message then print(tostring(result.message))end if result.cache then print(\"Cached release: \"..tostring(result.cache.version)..\"  \"..tostring(result.cache.fingerprint)..\"  key \"..tostring(result.cache.keyId))end if result.transfer then print(\"Download: \"..tostring(result.transfer.received or 0)..\"/\"..tostring(result.transfer.size)..\" bytes from \"..tostring(result.transfer.label or result.transfer.source))end end elseif sub==\"check\"then local ok,result=networkRequest(\"update_check\",{})if not ok then printError(result)elseif type(result)==\"table\"then print(\"Cloud offers: \"..tostring(result.cloud or 0)..\" | direct mesh query: \"..(result.direct and\"sent\"or\"unavailable\")..\" | routed queries: \"..tostring(result.routed or 0))if result.cloudError then printError(\"Cloud check failed: \"..tostring(result.cloudError))end else print(\"Signed release check started.\")end elseif sub==\"offers\"then local ok,result=networkRequest(\"update_offers\",{})if not ok then printError(result)elseif#result==0 then print(\"No newer signed releases discovered.\")else for _,offer in ipairs(result)do print(tostring(offer.id)..\"\\t\"..tostring(offer.label)..\"\\t\"..tostring(offer.version)..\"\\t\"..tostring(offer.size)..\" bytes\\t\"..tostring(offer.fingerprint)..\"\\t\"..tostring(offer.keyId)..\"\\t\"..tostring(offer.transport))end end elseif sub==\"download\"then requireArgs(3)local ok,result=networkRequest(\"update_download\",{source=args[3]})if ok then print(\"Download started. Run 'nx update status' to follow verified progress.\")else printError(result)end elseif sub==\"latest\"then local ok,result=networkRequest(\"update_install_latest\",{})if ok then print(\"Highest compatible signed release is downloading. Review it with 'nx update status', then run 'nx update apply'.\")else printError(result)end elseif sub==\"cancel\"then local ok,result=networkRequest(\"update_cancel\",{});if ok then print(\"Update download cancelled.\")else printError(result)end elseif sub==\"clear-cache\"then local ok,result=networkRequest(\"update_clear_cache\",{});if ok then print(\"Verified release cache cleared.\")else printError(result)end elseif sub==\"apply\"then local ok,status=networkRequest(\"update_status\",{})if not ok then printError(status)elseif not status.cache then printError(\"No verified signed release is cached.\")else print(\"Apply CeetOS \"..tostring(status.cache.version)..\" from verified cache\")print(\"Fingerprint: \"..tostring(status.cache.fingerprint)..\"  Key: \"..tostring(status.cache.keyId))term.write(\"Type APPLY to reboot and install locally: \")if read()~=\"APPLY\"then print(\"Cancelled.\")else local prepared,result=networkRequest(\"update_apply\",{})if not prepared then printError(result)else print(\"Verified update approved; rebooting locally.\");os.reboot()end end end else printError(\"use update status|check|offers|download <source>|latest|apply|cancel|clear-cache\")end elseif command==\"broker\"then local sub=args[2]or\"status\"if sub==\"status\"then local ok,result=networkRequest(\"broker_status\",{})if not ok then printError(result)else print(\"Release broker: \"..(result.enabled and\"enabled\"or\"disabled\")..\" | Cloud: \"..(result.enrolled and\"enrolled\"or\"not enrolled\"))for _,row in ipairs(result.pending or{})do print(\"Pending \"..row.id..\" from \"..row.label..\" (\"..row.profile..\")\")end end elseif sub==\"enable\"then auth.require(\"admin\");requireArgs(3)local ok,result=networkRequest(\"broker_promote\",{ownerKey=args[3]})if ok then print(\"This enrolled node is now a trusted release broker.\")else printError(result)end elseif sub==\"disable\"then auth.require(\"admin\");requireArgs(3)local ok,result=networkRequest(\"broker_revoke\",{ownerKey=args[3]})if ok then print(\"Release broker revoked.\")else printError(result)end elseif sub==\"approve\"then auth.require(\"admin\");requireArgs(4)local ok,result=networkRequest(\"broker_approve\",{session=args[3],phrase=args[4]})if ok then print(\"Bootstrap proof sent; waiting for the fresh computer to complete.\")else printError(result)end else printError(\"use broker status|enable <owner-key>|disable <owner-key>|approve <session> <phrase>\")end elseif command==\"pair\"then printError(\"legacy ID/code pairing is disabled. Use 'nx discover start <phrase>' and 'nx discover join <phrase>'.\")elseif command==\"share\"then auth.require(\"operator\");requireArgs(2)if args[2]==\"all\"then print(\"Shared \"..peripherals.shareAll(args[3]==\"master\"and\"master\"or\"peer\")..\" peripherals\")else peripherals.share(args[2],args[3]or\"peer\");print(\"Shared \"..args[2])end elseif command==\"unshare\"then auth.require(\"operator\");requireArgs(2)if args[2]==\"all\"then print(\"Unshared \"..peripherals.unshareAll()..\" peripherals\")else peripherals.unshare(args[2]);print(\"Unshared \"..args[2])end elseif command==\"peripherals\"then for _,p in ipairs(peripherals.describe())do print(p.name..\"\\t\"..p.type..\"\\t\"..p.mode)end elseif command==\"remote\"then auth.require(\"operator\");requireArgs(3)local peer,mode=args[2],args[3]if mode==\"peripherals\"then local ok,err=networkRequest(\"remote_request\",{target=peer,type=\"peripheral_list\",body={}});if not ok then printError(err)else print(\"Peripheral query sent. Run 'nx remote \"..peer..\" cache' after the reply arrives.\")end elseif mode==\"cache\"then local cache=store.read(\"/ceetos/data/remote/\"..peer..\".lua\",{})if not cache.peripherals then print(\"No peripheral cache for peer \"..peer..\"; run 'nx remote \"..peer..\" peripherals' first.\")else for _,p in ipairs(cache.peripherals)do print(p.name..\"\\t\"..p.type..\"\\t\"..p.mode)end end elseif mode==\"call\"then requireArgs(5)local values={}for i=6,#args do values[#values+1]=tonumber(args[i])or args[i]end local ok,err=networkRequest(\"remote_request\",{target=peer,type=\"peripheral_call\",body={name=args[4],method=args[5],args=values}})if not ok then printError(err)else print(\"Peripheral call sent; the result will be displayed and cached when it arrives.\")end else local cmd=table.concat(args,\" \",3);local ok,err=networkRequest(\"remote_request\",{target=peer,type=\"shell_request\",body={command=cmd}});if not ok then printError(err)else print(\"Request sent.\")end end elseif command==\"jobs\"then local sub=args[2]or\"status\"local function controllerOnly()auth.require(\"admin\")assert(tostring(net.controller())==tostring(os.getComputerID()),\"this computer is not the network job controller (\"..tostring(net.controller())..\")\")end if sub==\"status\"then auth.require(\"operator\")local state=jobs.state();print(\"Controller: \"..tostring(net.controller())..\" | queue: \"..#(state.queue or{}))for id,worker in pairs(jobs.workers())do print(tostring(id)..\"\\t\"..tostring(worker.inFlight or 0)..\"/\"..tostring(worker.capacity or 1)..\"\\t\"..(worker.healthy and\"healthy\"or\"stale\"))end elseif sub==\"history\"then auth.require(\"operator\")for _,item in ipairs(jobs.history())do print(item.id..\"\\t\"..item.template..\"\\t\"..item.status..\"\\t\"..tostring(item.result and(item.result.error or item.result.output)or\"\"):sub(1,100))end elseif sub==\"templates\"then auth.require(\"operator\")for name,item in pairs(jobs.listTemplates())do print(name..\"\\t\"..(item.enabled==false and\"off\"or\"on\")..\"\\t\"..(item.retrySafe and\"retry-safe\"or\"no-retry\"))end elseif sub==\"template\"and args[3]==\"add\"then controllerOnly();requireArgs(5);local commandLine=table.concat(args,\" \",5)local template=jobs.addTemplate(args[4],commandLine,{});audit.log(auth.current.name,\"jobs.template_add\",{name=template.name});print(\"Template added.\")elseif sub==\"template\"and args[3]==\"retry-safe\"then controllerOnly();requireArgs(5);jobs.setTemplateRetrySafe(args[4],args[5]==\"on\");print(\"Retry policy updated.\")elseif sub==\"template\"and args[3]==\"enable\"then controllerOnly();requireArgs(5);jobs.setTemplateEnabled(args[4],args[5]~=\"off\");print(\"Template state updated.\")elseif sub==\"run\"then controllerOnly();requireArgs(3);local job=jobs.enqueue(args[3]);print(\"Queued \"..job.id)elseif sub==\"schedule\"then controllerOnly();requireArgs(4);local schedule=jobs.schedule(args[3],tonumber(args[4]));print(\"Scheduled \"..schedule.id)elseif sub==\"cancel\"then controllerOnly();requireArgs(3);jobs.cancel(args[3]);print(\"Schedule cancelled.\")elseif sub==\"capacity\"then auth.require(\"admin\");requireArgs(3);print(\"Worker capacity: \"..jobs.setCapacity(tonumber(args[3])))elseif sub==\"controller\"then controllerOnly();requireArgs(3);print(\"Controller: \"..net.setController(args[3]))else printError(\"use jobs status|history|templates|template add <name> <fixed command>|template retry-safe <name> on|off|run <name>|schedule <name> <seconds>|cancel <id>|capacity <slots>|controller <id>\")end elseif command==\"telemetry\"then requireArgs(2)if args[2]==\"status\"then auth.require(\"operator\")local status=cloudSync.status()print(\"Telemetry: \"..(telemetry.status()and\"enabled\"or\"disabled\")..\" | enrolled \"..(status.enrolled and\"yes\"or\"no\"))print(\"Last report: \"..tostring(status.telemetryLastSuccess or\"never\")..\" | next: \"..tostring(status.telemetryNext or\"disabled\"))if status.telemetryLastError then print(\"Last telemetry error: \"..status.telemetryLastError)end elseif args[2]==\"on\"or args[2]==\"off\"then auth.require(\"admin\");telemetry.setEnabled(args[2]==\"on\");audit.log(auth.current.name,\"telemetry\",{enabled=args[2]==\"on\"});print(\"Telemetry \"..args[2])else error(\"use telemetry status, telemetry on, or telemetry off\",0)end elseif command==\"cloud\"then local sub=args[2]or\"status\"if sub==\"status\"then auth.require(\"operator\")local status=cloudSync.status()print(\"Cloud: \"..(status.enrolled and\"enrolled\"or\"not enrolled\")..\" | auth \"..(status.authOnline and\"online\"or\"offline\")..\" | recipes \"..(status.recipeOnline and\"online\"or\"offline\"))if status.node then print(\"Node: \"..status.node)end if status.lastError then print(\"Last error: \"..status.lastError)end print(\"Telemetry: \"..(status.telemetryEnabled and\"enabled\"or\"disabled\")..\" | last \"..tostring(status.telemetryLastSuccess or\"never\")..\" | next \"..tostring(status.telemetryNext or\"disabled\"))print(\"Dashboard: https://dash.ceet.uk (Cloudflare Access)\")elseif sub==\"enroll\"then auth.require(\"admin\");requireArgs(3)local status,err=cloudSync.enroll(args[3]);if not status then printError(err)else audit.log(auth.current.name,\"cloud.enroll\",{node=status.node});print(\"Cloud node enrollment saved. Background sync is enabled.\")end elseif sub==\"retry\"then auth.require(\"operator\")os.queueEvent(\"ceetos_cloud_sync\")print(\"Cloud sync requested.\")elseif sub==\"clear\"then auth.require(\"admin\")local ok,err=cloudSync.clear();if ok then audit.log(auth.current.name,\"cloud.clear\",{});print(\"Cloud enrollment removed.\")else printError(err)end else error(\"use cloud status|enroll <one-time-code>|retry|clear\",0)end elseif command==\"owner-service\"then auth.require(\"admin\")local sub=args[2]or\"status\"if sub==\"status\"then local status=ownerService.status()print(\"Owner service: \"..(status.enabled and\"enabled\"or\"disabled\")..\" | \"..(status.provisioned and\"provisioned\"or\"sealed\"))if status.owner then print(\"Owner: \"..status.owner)end print(\"Actions: \"..table.concat(status.actions or{},\", \"))elseif sub==\"on\"or sub==\"off\"then ownerService.setEnabled(sub==\"on\")audit.log(auth.current.name,\"owner-service.enabled\",{enabled=sub==\"on\"})print(\"Owner service \"..sub..\".\")elseif sub==\"set-token\"then term.write(\"Owner-service token: \");local token=read(\"*\")term.write(\"Repeat token: \");local repeatToken=read(\"*\")assert(token==repeatToken,\"tokens do not match\")ownerService.provision(token,auth.current.name)audit.log(auth.current.name,\"owner-service.provision\",{})print(\"Owner service provisioned. Configure the same secret in CEETOS_SERVICE_TOKENS on the dashboard.\")elseif sub==\"rotate-token\"then local token=ownerService.rotate(auth.current.name)audit.log(auth.current.name,\"owner-service.rotate\",{})print(\"New owner-service token (shown once): \"..token)print(\"Update CEETOS_SERVICE_TOKENS on the dashboard before making remote requests.\")else error(\"use owner-service status|on|off|set-token|rotate-token\",0)end elseif command==\"recover\"and args[2]==\"enable\"then auth.require(\"admin\");local minutes=tonumber(args[3])or 10;assert(minutes>0 and minutes<=60,\"minutes must be 1-60\")store.write(\"/ceetos/data/recovery.lua\",{until_ts=os.epoch(\"utc\")+minutes*60000});audit.log(auth.current.name,\"recovery.enable\",{minutes=minutes});print(\"Recovery access enabled for \"..minutes..\" minutes.\")elseif command==\"recover\"and args[2]==\"reset\"then requireArgs(3)local recovery=store.read(\"/ceetos/data/recovery.lua\",{})assert((recovery.until_ts or 0)>os.epoch(\"utc\"),\"recovery window is not active\")term.write(\"New password for \"..args[3]..\": \");local pass=read(\"*\")assert(#pass>=8,\"password must contain at least 8 characters\")auth.resetPassword(args[3],pass)audit.log(\"local-recovery\",\"password.reset\",{name=args[3]})print(\"Password reset. The recovery window remains active until it expires.\")else usage()end",
  ["ceetos/bin/ceetui.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local gui=require(\"ceetos.lib.gui\")local auth=require(\"ceetos.lib.auth\")local net=require(\"ceetos.lib.net\")local peripherals=require(\"ceetos.lib.peripherals\")local telemetry=require(\"ceetos.lib.telemetry\")telemetry.markActivity(\"desktop\")local cloudSync=require(\"ceetos.lib.cloud_sync\")local audit=require(\"ceetos.lib.audit\")local store=require(\"ceetos.lib.store\")local files=require(\"ceetos.lib.files\")local clipboard=require(\"ceetos.lib.clipboard\")local networkIpc=require(\"ceetos.lib.network_ipc\")local terminalUi=require(\"ceetos.lib.terminal_ui\")local release=require(\"ceetos.lib.release_verify\")local input=require(\"ceetos.lib.input\")local pairingMonitor=require(\"ceetos.lib.pairing_monitor\")local modalInput=require(\"ceetos.lib.modal_input\")local DROP_STAGING=\"/ceetos/data/drop-staging\"local MAX_DROPPED_UPDATE_BYTES=2*1024*1024 local launch={...}local state=gui.newState()local page=launch[1]==\"files\"and\"files\"or\"home\"if page==\"files\"and launch[2]and fs.exists(launch[2])then local target=launch[2]state.path=fs.isDir(target)and target or fs.getDir(target)state.selected=\"file:\"..target end local banner,targets,currentModel=nil,{},nil local remotePending=nil local function deps()return{auth=auth,net=net,peripherals=peripherals,telemetry=telemetry,cloud=cloudSync,audit=audit,files=files}end local function refresh(message,error,forceRedraw)if forceRedraw then gui.invalidate(state)end banner=message and{text=message,error=error}or nil currentModel=gui.model(page,deps(),state)targets=gui.draw(currentModel,state,banner)end local function fill(x,y,width,height,background)return terminalUi.fill(x,y,width,height,background)end local function waitForModalSize(cancelValue)while true do local width,height=term.getSize()if width>=24 and height>=9 then return true end term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear()terminalUi.minimum(\"CeetOS\",\"This prompt needs 24x9\",\"Resize | Backspace cancel\")local event,code=os.pullEvent()if event==\"key\"and input.navigation(keys.getName(code))==\"backspace\"then return false,cancelValue end end end local function stageDroppedUpdate(transfer)if not transfer or type(transfer.read)~=\"function\"then return nil,\"Dropped update is not readable\"end if not fs.exists(DROP_STAGING)then fs.makeDir(DROP_STAGING)end local path=fs.combine(DROP_STAGING,\"update-\"..tostring(os.epoch(\"utc\"))..\".tmp\")local handle=fs.open(path,\"w\")if not handle then return nil,\"Could not stage dropped update\"end local total=0 while true do local chunk=transfer.read(8192)if not chunk then break end if type(chunk)~=\"string\"then handle.close();fs.delete(path);return nil,\"Dropped update has invalid data\"end total=total+#chunk if total>MAX_DROPPED_UPDATE_BYTES then handle.close();fs.delete(path);return nil,\"Dropped update exceeds the 2 MiB limit\"end handle.write(chunk)end handle.close()return path end local function readStagedUpdate(path)local handle=path and fs.open(path,\"r\")or nil if not handle then return nil,\"Could not read staged update\"end local source=handle.readAll();handle.close()return source end local function prompt(label,secret,initial)if not waitForModalSize(nil)then gui.invalidate(state);return nil end local value,accepted=modalInput.read({initial=initial,secret=secret,limit=160,draw=function(text,cursor,masked)gui.draw(currentModel,state,banner)local width,height=term.getSize()local bounds=gui.modalBounds(width,height,math.max(20,#tostring(label)+8),6)local boxWidth,boxHeight,x,y=bounds.width,bounds.height,bounds.x,bounds.y fill(x,y,boxWidth,boxHeight,colors.black)terminalUi.box(x,y,x+boxWidth-1,y+boxHeight-1,masked and\"Secure input\"or\"Input\",colors.gray,colors.lightBlue)terminalUi.write(x+2,y+1,label,colors.white,colors.black,boxWidth-4)terminalUi.write(x+2,y+boxHeight-2,\"Enter confirm | Empty Backspace cancels\",colors.gray,colors.black,boxWidth-4)local shown=masked and string.rep(\"*\",#text)or text local available=math.max(1,boxWidth-4)local start=math.max(1,cursor-available+2)shown=shown:sub(start,start+available-1)term.setTextColor(colors.white);term.setBackgroundColor(colors.black)terminalUi.write(x+2,y+2,shown,colors.white,colors.black,available)term.setCursorPos(math.min(x+available+1,x+2+cursor-start+1),y+2)term.setCursorBlink(true)end,})term.setCursorBlink(false)gui.invalidate(state)return accepted and value or nil end local function choose(label,options)if#options==0 then return nil end local selected,scroll=1,1 local function draw()gui.draw(currentModel,state,banner)local width,height=term.getSize();local bounds=gui.modalBounds(width,height,42,math.min(#options+4,height-2));local boxWidth,x,y,boxHeight=bounds.width,bounds.x,bounds.y,bounds.height fill(x,y,boxWidth,boxHeight,colors.black)terminalUi.box(x,y,x+boxWidth-1,y+boxHeight-1,\"Choose\",colors.gray,colors.lightBlue)terminalUi.write(x+2,y+1,label,colors.white,colors.black,boxWidth-4)local visibleChoices=math.max(1,boxHeight-3)scroll=math.max(1,math.min(math.max(1,#options-visibleChoices+1),selected-math.floor(visibleChoices/2)))for row=1,visibleChoices do local index,option=scroll+row-1,options[scroll+row-1]if not option then break end terminalUi.clearLine(y+row+1,index==selected and colors.gray or colors.black)terminalUi.write(x+2,y+row+1,tostring(option.label or option),index==selected and colors.white or colors.lightGray,index==selected and colors.gray or colors.black,boxWidth-4)end return x,y,boxWidth,visibleChoices,scroll end local x,y,boxWidth,visibleChoices,firstChoice=draw()while true do local event,first,second,third=os.pullEvent()if event==\"key\"then local key=input.navigation(keys.getName(first))if key==\"up\"then selected=math.max(1,selected-1);x,y,boxWidth,visibleChoices,firstChoice=draw()elseif key==\"down\"then selected=math.min(#options,selected+1);x,y,boxWidth,visibleChoices,firstChoice=draw()elseif key==\"enter\"then gui.invalidate(state);return options[selected].value or options[selected]elseif key==\"backspace\"then gui.invalidate(state);return nil end elseif event==\"mouse_click\"and second>=x and second<=x+boxWidth and third>=y+2 and third<=y+visibleChoices+1 then local index=firstChoice+third-y-2;if options[index]then gui.invalidate(state);return options[index].value or options[index]end elseif event==\"term_resize\"then x,y,boxWidth,visibleChoices,firstChoice=draw()end end end local networkRequestCounter=0 local function networkRequest(action,payload)networkRequestCounter=networkRequestCounter+1 local request=payload or{}request.id=\"ui-\"..tostring(os.epoch(\"utc\"))..\"-\"..tostring(networkRequestCounter)request.action=action local submitted,submitErr=networkIpc.submit(request)if not submitted then return false,submitErr or\"CeetOS network service is unavailable\"end local result,waitErr=networkIpc.await(request.id,5000)if waitErr then return false,waitErr end return result and result.ok==true,result and(result.result or result.error)or\"CeetOS network service did not respond\"end local function activeDiscoveryControl()local snapshot=(store.readStable and store.readStable(\"/ceetos/data/network-status.lua\",nil))or store.read(\"/ceetos/data/network-status.lua\",nil)local session=type(snapshot)==\"table\"and type(snapshot.discovery)==\"table\"and snapshot.discovery.session or nil local control=type(session)==\"table\"and session.control or nil if type(control)~=\"string\"or control==\"\"then return nil,\"No active discovery session to cancel\"end return control end local function beginPairingMonitor(detail)local pending,message=pairingMonitor.begin(detail,os.epoch(\"utc\"),120000)if not pending then return message end state.pairingPending=pending return\"Waiting for pairing confirmation from \"..state.pairingPending.label end local function pollPairingMonitor()local pending=state.pairingPending if not pending then return false end local snapshot=(store.readStable and store.readStable(\"/ceetos/data/network-status.lua\",nil))or store.read(\"/ceetos/data/network-status.lua\",nil)local changed,message,isError=pairingMonitor.poll(pending,snapshot,os.epoch(\"utc\"))if changed then state.pairingPending=nil end return changed,message,isError end local function displayRemoteResult(result)if type(result)~=\"table\"then return tostring(result or\"\")end if result.ok==false then return tostring(result.result or result.error or\"request failed\")end local value=result.result local text=type(value)==\"table\"and textutils.serialise(value,{compact=true})or tostring(value)return terminalUi.clean(text):sub(1,240)end local function confirm(label)local function draw()gui.draw(currentModel,state,banner)local width,height=term.getSize()local bounds=gui.modalBounds(width,height,math.max(20,#label+4),6)local boxWidth,boxHeight,x,y=bounds.width,bounds.height,bounds.x,bounds.y fill(x,y,boxWidth,boxHeight,colors.black)terminalUi.box(x,y,x+boxWidth-1,y+boxHeight-1,\"Confirm action\",colors.gray,colors.lightBlue)terminalUi.write(x+2,y+1,label,colors.white,colors.black,boxWidth-4)local confirmX,cancelX,buttonY=x+2,x+math.floor(boxWidth/2)+1,y+boxHeight-2 term.setBackgroundColor(colors.green);term.setTextColor(colors.black);term.setCursorPos(confirmX,buttonY);term.write(\" Confirm \")term.setBackgroundColor(colors.gray);term.setTextColor(colors.white);term.setCursorPos(cancelX,buttonY);term.write(\" Cancel \")term.setBackgroundColor(colors.black);term.setTextColor(colors.lightGray);term.setCursorPos(x+2,y+2);term.write(\"Click a choice or press Enter / Backspace\")term.setTextColor(colors.white)return{confirmX=confirmX,cancelX=cancelX,y=buttonY}end if not waitForModalSize(false)then gui.invalidate(state);return false end local modal=draw()while true do local event,first,second,third=os.pullEvent()if event==\"mouse_click\"and third==modal.y then if second>=modal.confirmX and second<=modal.confirmX+8 then gui.invalidate(state);return true end if second>=modal.cancelX and second<=modal.cancelX+7 then gui.invalidate(state);return false end elseif event==\"key\"then local key=keys.getName(first)if key==\"enter\"or key==\"y\"then gui.invalidate(state);return true end if key==\"backspace\"or key==\"n\"then gui.invalidate(state);return false end elseif event==\"char\"then if first:lower()==\"y\"then gui.invalidate(state);return true elseif first:lower()==\"n\"then gui.invalidate(state);return false end elseif event==\"term_resize\"then modal=draw()end end end local function chooseRole()return choose(\"Choose account role\",{{label=\"Viewer  - run approved programs\",value=\"viewer\"},{label=\"Operator - manage local users and tools\",value=\"operator\"},{label=\"Admin    - full CeetOS administration\",value=\"admin\"},})end local function values(text)local out={}for item in(text or\"\"):gmatch(\"[^,]+\")do out[#out+1]=tonumber(item)or item end return out end local function openMenu(item)local items=gui.context(item)if#items==0 then return refresh(\"No actions available for \"..tostring(item and item.label or\"item\"),true)end state.menu={items=items,selected=1}refresh()end local function cacheFor(peer)local cachePath=\"/ceetos/data/remote/\"..tostring(peer)..\".lua\"store.invalidate(cachePath)return store.read(cachePath,{}),cachePath end local function beginRemoteRequest(kind,peer,body,continueCall)networkRequestCounter=networkRequestCounter+1 local requested=os.epoch(\"utc\")local request={id=\"ui-remote-\"..tostring(requested)..\"-\"..tostring(networkRequestCounter),action=\"remote_request\",target=peer,type=kind==\"list\"and\"peripheral_list\"or\"peripheral_call\",body=body,}local sent,err=networkIpc.submit(request)if not sent then state.remoteResult={status=\"error\",error=true,peer=tostring(peer),text=err or\"CeetOS network service is unavailable\"}return false end local cache,cachePath=cacheFor(peer)remotePending={kind=kind,peer=peer,body=body,continueCall=continueCall==true,id=request.id,requested=requested,cachePath=cachePath,phase=\"dispatch\",deadline=requested+5000,cache=cache}state.remoteResult={status=\"pending\",pending=true,peer=tostring(peer),device=body and body.name,method=body and body.method,text=kind==\"list\"and\"Requesting peripheral list...\"or\"Calling remote peripheral...\"}return true end local function chooseRemoteCall(peer,cache)local choices={}for _,item in ipairs(cache.peripherals or{})do choices[#choices+1]={label=tostring(item.name)..\" (\"..tostring(item.type or\"unknown\")..\")\",value=item}end local device=choose(\"Choose peripheral\",choices);if not device then return false end local methods={};for _,method in ipairs(device.methods or{})do methods[#methods+1]={label=method,value=method}end local method=choose(\"Choose function\",methods);if not method then return false end local arguments=prompt(\"Arguments (comma separated)\")if arguments==nil then return false end return beginRemoteRequest(\"call\",peer.id or peer,{name=device.name,method=method,args=values(arguments)})end local function pollRemoteRequest()if not remotePending then return false end local pending,now=remotePending,os.epoch(\"utc\")if now>=pending.deadline then state.remoteResult={status=\"error\",error=true,peer=tostring(pending.peer),device=pending.body and pending.body.name,method=pending.body and pending.body.method,text=pending.kind==\"list\"and\"No peripheral-list reply received\"or\"No peripheral-call reply received\"}remotePending=nil return true end if pending.phase==\"dispatch\"then local result=networkIpc.poll(pending.id,true)if result then if result.ok~=true then state.remoteResult={status=\"error\",error=true,peer=tostring(pending.peer),text=tostring(result.error or\"Remote request was rejected\")}remotePending=nil return true end pending.phase=\"reply\"return true end return false end store.invalidate(pending.cachePath)local cache=store.read(pending.cachePath,{})if cache.updated and cache.updated>=pending.requested and(pending.kind~=\"list\"or type(cache.peripherals)==\"table\")and(pending.kind~=\"call\"or cache.last_result~=nil)then remotePending=nil if pending.kind==\"list\"and pending.continueCall then state.remoteResult={status=\"ready\",peer=tostring(pending.peer),text=\"Peripheral list received. Choose a device.\"}chooseRemoteCall({id=pending.peer},cache)elseif pending.kind==\"list\"then state.remoteResult={status=\"ready\",peer=tostring(pending.peer),text=tostring(#(cache.peripherals or{}))..\" shared peripherals available\"}else local result=cache.last_result state.remoteResult={status=result and result.ok==false and\"error\"or\"complete\",error=result and result.ok==false or false,peer=tostring(pending.peer),device=pending.body and pending.body.name,method=pending.body and pending.body.method,text=displayRemoteResult(result)}end return true end return false end local function run(action)state.menu=nil if action==\"login\"and state.authPending then return refresh(\"Authentication is already in progress\")end if action:sub(1,5)==\"page:\"then page=action:sub(6);state.selected=nil;return refresh()end if action==\"terminal\"then term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear();term.setCursorPos(1,1)local ok,result=pcall(shell.run,\"/ceetos/bin/ceetshell.lua\")gui.invalidate(state)if not ok or result==false then return refresh(\"Terminal failed to start: \"..tostring(result or\"runtime error\"),true,true)end return refresh(nil,nil,true)end if action==\"crafting\"then term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear();term.setCursorPos(1,1)local ok,result=pcall(shell.run,\"/ceetos/bin/ceetcraft.lua\")gui.invalidate(state)if not ok or result==false then return refresh(\"Crafting console failed to start\",true,true)end return refresh(nil,nil,true)end if action==\"peripheral_view\"then state.peripheralView=true;state.selected=nil;return refresh()end if action==\"trash_open\"then state.trash=true;state.selected=nil;return refresh()end if action:sub(1,3)==\"up:\"then state.path=table.remove(state.history)or action:sub(4);state.selected=nil;return refresh()end if action:sub(1,4)==\"dir:\"then state.history[#state.history+1]=state.path;state.path=action:sub(5);state.selected=nil;return refresh()end if action:sub(1,4)==\"run:\"then if not auth.allowed(\"viewer\")then return refresh(\"Login required to run programs\",true)end local path=action:sub(5)if not files.runnable(path)then return refresh(\"This file is not a runnable Lua program\",true)end term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear();term.setCursorPos(1,1)shell.run(\"/ceetos/bin/ceetlaunch.lua\",path)gui.invalidate(state)return refresh(nil,nil,true)end if action:sub(1,5)==\"edit:\"then local path=action:sub(6)term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear();term.setCursorPos(1,1)shell.run(\"/ceetos/bin/ceetedit.lua\",path)gui.invalidate(state)return refresh(nil,nil,true)end local ok,result,actionError=pcall(function()if action:sub(1,11)==\"copy_label:\"then local id=action:sub(12);local item=currentModel and gui.selected(currentModel,state)if item and item.id==id then clipboard.set(item.label or\"\");return\"Copied\"end return\"Nothing selected\"elseif action:sub(1,11)==\"copy_text:\"then clipboard.set(action:sub(12));return\"Copied\"elseif action==\"remote_result_copy\"then local result=state.remoteResult if not result or not result.text then return\"No remote result to copy\"end clipboard.set(result.text);return true elseif action==\"remote_result_clear\"then state.remoteResult=nil;return true elseif action:sub(1,7)==\"rename:\"then auth.require(\"operator\")local path,name=action:sub(8),prompt(\"New name\")if name==nil then return\"Cancelled\"end if not confirm(\"Rename \"..fs.getName(path)..\" to \"..name)then return\"Cancelled\"end local target=files.rename(path,name);state.selected=\"file:\"..target;return target elseif action:sub(1,6)==\"trash:\"then auth.require(\"operator\")local path=action:sub(7)if not confirm(\"Move \"..fs.getName(path)..\" to Trash\")then return\"Cancelled\"end local id=files.moveToTrash(path);state.selected=nil;return\"Moved to Trash\"elseif action:sub(1,8)==\"restore:\"then auth.require(\"operator\")local path=files.restore(action:sub(9));return\"Restored \"..fs.getName(path)elseif action:sub(1,13)==\"delete_trash:\"then auth.require(\"operator\")if not confirm(\"Delete this Trash item permanently\")then return\"Cancelled\"end files.deleteTrash(action:sub(14));return true elseif action==\"repair_trash\"then auth.require(\"admin\")if not confirm(\"Repair pending Trash transactions\")then return\"Cancelled\"end local repaired=files.repairTrash()return repaired>0 and(\"Repaired \"..tostring(repaired)..\" Trash transaction(s)\")or\"No repairable Trash transactions\"elseif action==\"new_folder\"then auth.require(\"operator\")local name=prompt(\"Folder name\")if name==nil then return\"Cancelled\"end if not confirm(\"Create folder \"..name)then return\"Cancelled\"end return files.makeDirectory(state.path,name)elseif action==\"empty_trash\"then auth.require(\"operator\")if not confirm(\"Permanently delete all Trash items\")then return\"Cancelled\"end files.emptyTrash();return true elseif action==\"start_discovery\"then local phrase=prompt(\"Choose discovery phrase\")if phrase==nil then return\"Cancelled\"end local mode=prompt(\"Mode (peer/master)\")if mode==nil then return\"Cancelled\"end local master=mode==\"master\"and prompt(\"Master side (initiator/joiner)\")or\"initiator\"if master==nil then return\"Cancelled\"end if not confirm(\"Start \"..(mode==\"master\"and\"master/slave\"or\"peer\")..\" discovery with this phrase\")then return\"Cancelled\"end local ok,err=networkRequest(\"discovery_start\",{phrase=phrase,mode=mode,master=master});if ok then return\"Discovery active (waiting for join)\"end;return err elseif action==\"join_discovery\"then local phrase=prompt(\"Discovery phrase\",true)if phrase==nil then return\"Cancelled\"end local ok,result=networkRequest(\"discovery_join\",{phrase=phrase})if not ok then return result end if type(result)==\"table\"and result.state==\"candidate\"then local candidate=result.candidate or{}local peerLabel=tostring(candidate.label or(\"Computer \"..tostring(candidate.id or\"?\")))local relationship=candidate.mode==\"master\"and\"master/slave\"or\"equal-peer\"if not confirm(\"Pair directly with \"..peerLabel..\" (\"..relationship..\")\")then return\"Pairing confirmation cancelled\"end local confirmed,confirmInfo=networkRequest(\"discovery_confirm\",{})return confirmed and beginPairingMonitor(confirmInfo)or confirmInfo end return\"Searching for a matching discovery session\"elseif action==\"confirm_discovery\"then if not confirm(\"Confirm the selected direct pairing\")then return\"Cancelled\"end local ok,detail=networkRequest(\"discovery_confirm\",{})return ok and beginPairingMonitor(detail)or detail elseif action==\"cancel_discovery\"then local control,controlErr=activeDiscoveryControl()if not control then return controlErr end local cancelled,cancelErr=networkRequest(\"discovery_cancel\",{control=control,source=\"ceetui\"})return cancelled and\"Discovery cancelled\"or cancelErr elseif action==\"discover\"then return net.discover()elseif action==\"pair_offer\"then local id=prompt(\"Peer ID\")if id==nil then return\"Cancelled\"end local code=prompt(\"Pair code\",true)if code==nil then return\"Cancelled\"end if not confirm(\"Send pairing offer\")then return\"Cancelled\"end;return net.offer(id,code)elseif action==\"pair_accept\"then local id=prompt(\"Peer ID\")if id==nil then return\"Cancelled\"end local code=prompt(\"Pair code\",true)if code==nil then return\"Cancelled\"end local peerRole=prompt(\"Role (viewer/operator/admin)\")if peerRole==nil then return\"Cancelled\"end local master=confirm(\"Is this peer the master\")if not confirm(\"Accept pairing\")then return\"Cancelled\"end;return net.accept(id,code,peerRole==\"\"and\"operator\"or peerRole,master)elseif action==\"remote_refresh\"then local live=store.read(\"/ceetos/data/network-status.lua\",{});local choices={}for _,peer in ipairs(live.peers or net.peers())do choices[#choices+1]={label=(peer.label or(\"Computer \"..peer.id))..\" (\"..peer.id..\")\",value=peer.id}end local peer=choose(\"Refresh peripherals from\",choices);if not peer then return false end return beginRemoteRequest(\"list\",peer)elseif action:sub(1,15)==\"peripheral_set:\"then local name,mode=action:match(\"^peripheral_set:(.-):(.+)$\")if mode~=\"peer\"and mode~=\"master\"then return\"Invalid sharing mode\"end local current for _,item in ipairs(peripherals.describe())do if item.name==name then current=item.mode end end if current==mode then peripherals.unshare(name);return\"Unshared \"..name end peripherals.share(name,mode);return\"Shared \"..name..(mode==\"master\"and\" (master only)\"or\" (peers)\")elseif action:sub(1,19)==\"peripheral_unshare:\"then local name=action:sub(20)peripherals.unshare(name);return\"Unshared \"..name elseif action==\"share_all:peer\"or action==\"share_all:master\"then local mode=action:sub(11)if not confirm(\"Share every detected peripheral\"..(mode==\"master\"and\" with master only\"or\" with peers\"))then return\"Cancelled\"end return\"Shared \"..peripherals.shareAll(mode)..\" peripherals\"elseif action==\"unshare_all\"then if not confirm(\"Stop sharing every peripheral\")then return\"Cancelled\"end return\"Unshared \"..peripherals.unshareAll()..\" peripherals\"elseif action==\"remote_cache\"then local live,choices=store.read(\"/ceetos/data/network-status.lua\",{}),{}for _,peer in ipairs(live.peers or net.peers())do choices[#choices+1]={label=(peer.label or(\"Computer \"..peer.id))..\" (\"..peer.id..\")\",value=peer.id}end local peer=choose(\"View cached peripherals from\",choices);if not peer then return false end local cache=cacheFor(peer)state.remoteResult={status=\"ready\",peer=tostring(peer),text=tostring(#(cache.peripherals or{}))..\" cached peripherals\"}return true elseif action==\"remote_call\"then local live,peers=store.read(\"/ceetos/data/network-status.lua\",{}),{}for _,peer in ipairs(live.peers or net.peers())do peers[#peers+1]={label=(peer.label or(\"Computer \"..peer.id))..\" (\"..peer.id..\")\",value=peer}end local peer=choose(\"Choose peer\",peers);if not peer then return false end local cache=cacheFor(peer.id)if not cache.peripherals or#cache.peripherals==0 then return beginRemoteRequest(\"list\",peer.id,nil,true)end return chooseRemoteCall(peer,cache)elseif action==\"update_check\"then auth.require(\"operator\")local checked,err=networkRequest(\"update_check\",{})if not checked then return err end if type(err)==\"table\"then if err.cloudError then return\"Cloud check failed: \"..tostring(err.cloudError)end return\"Found \"..tostring(err.cloud or 0)..\" cloud offer(s); mesh check sent to \"..tostring(err.routed or 0)..\" peer(s)\"end return\"Signed release check started\"elseif action==\"update_install_latest\"then auth.require(\"operator\")local started,err=networkRequest(\"update_install_latest\",{})return started and\"Latest compatible signed release is downloading; apply remains locally confirmed\"or err elseif action:sub(1,16)==\"update_download:\"then auth.require(\"operator\")local source=action:sub(17)local started,err=networkRequest(\"update_download\",{source=source})return started and\"Verified download started\"or err elseif action==\"update_cancel\"then auth.require(\"operator\")local cancelled,err=networkRequest(\"update_cancel\",{})return cancelled and\"Update download cancelled\"or err elseif action==\"update_clear\"then auth.require(\"operator\")if not confirm(\"Clear the verified release cache\")then return\"Cancelled\"end local cleared,err=networkRequest(\"update_clear_cache\",{})return cleared and\"Release cache cleared\"or err elseif action==\"update_apply\"then auth.require(\"operator\")local statusOk,update=networkRequest(\"update_status\",{})if not statusOk or not update.cache then return(statusOk and\"No verified release is cached\"or update)end local label=\"Apply CeetOS \"..tostring(update.cache.version)..\" (\"..tostring(update.cache.fingerprint)..\", key \"..tostring(update.cache.keyId)..\")\"if not confirm(label)then return\"Cancelled\"end local prepared,err=networkRequest(\"update_apply\",{})if not prepared then return err end term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear();term.setCursorPos(1,1)print(\"Verified release approved. Rebooting locally...\")sleep(0.3);os.reboot()elseif action==\"telemetry_toggle\"then if not confirm(\"Change telemetry state\")then return\"Cancelled\"end;telemetry.setEnabled(not telemetry.status());audit.log(auth.current.name,\"telemetry\",{enabled=telemetry.status()});return true elseif action==\"cloud_enroll\"then auth.require(\"admin\")local code=prompt(\"One-time Cloud enrollment code\",true)if code==nil then return\"Cancelled\"end if not confirm(\"Enroll this computer with auth.ceet.uk\")then return\"Cancelled\"end local enrolled,enrollErr=cloudSync.enroll(code)if not enrolled then return enrollErr end audit.log(auth.current.name,\"cloud.enroll\",{node=enrolled.node})return\"Cloud enrollment saved; syncing in the background\"elseif action==\"cloud_retry\"then auth.require(\"operator\")os.queueEvent(\"ceetos_cloud_sync\")return\"Cloud sync requested\"elseif action==\"cloud_clear\"then auth.require(\"admin\")if not confirm(\"Remove this computer's Cloud enrollment\")then return\"Cancelled\"end local cleared,clearErr=cloudSync.clear()if cleared then audit.log(auth.current.name,\"cloud.clear\",{})end return cleared and\"Cloud enrollment removed\"or clearErr elseif action==\"login\"then local name=prompt(\"User\")if name==nil or name==\"\"then return\"Login cancelled\"end local password=prompt(\"Password\",true)if password==nil or password==\"\"then return\"Login cancelled\"end if auth.centralActive()and not cloudSync.authOnline()then state.authPending=auth.startLogin(name,password)banner={text=\"Contacting Auth Server...\",error=false}return\"Authentication request started\"end local loggedIn,loginErr=auth.login(name,password)if not loggedIn then return loginErr or\"Login failed\"end return\"Signed in\"elseif action==\"logout\"then if not confirm(\"Logout\")then return\"Cancelled\"end;auth.logout();return true elseif action==\"user_add\"then local name=prompt(\"User name\")if name==nil then return\"Cancelled\"end local userRole=chooseRole()if not userRole then return\"Cancelled\"end local password=prompt(\"Password\",true)if password==nil then return\"Cancelled\"end if not confirm(\"Create user \"..name)then return\"Cancelled\"end;auth.add(name,userRole,password);audit.log(auth.current.name,\"user.add\",{name=name,role=userRole});return true elseif action:sub(1,10)==\"user_role:\"then local name,userRole=action:match(\"^user_role:(.-):(.+)$\")if not confirm(\"Change \"..name..\" to \"..userRole)then return\"Cancelled\"end;auth.setRole(name,userRole);audit.log(auth.current.name,\"user.role\",{name=name,role=userRole});return true elseif action:sub(1,11)==\"user_reset:\"then local name,password=action:sub(12),prompt(\"New password for \"..action:sub(12),true)if password==nil then return\"Cancelled\"end if not confirm(\"Reset password for \"..name)then return\"Cancelled\"end;auth.adminResetPassword(name,password);audit.log(auth.current.name,\"user.password\",{name=name});return true elseif action:sub(1,11)==\"user_delete:\"then local name=action:sub(12)if not confirm(\"Delete user \"..name)then return\"Cancelled\"end;auth.remove(name);audit.log(auth.current.name,\"user.delete\",{name=name});return true elseif action==\"recovery_enable\"then local rawMinutes=prompt(\"Recovery minutes (1-60)\")if rawMinutes==nil then return\"Cancelled\"end local minutes=tonumber(rawMinutes)or 10 if not confirm(\"Enable recovery window\")then return\"Cancelled\"end;store.write(\"/ceetos/data/recovery.lua\",{until_ts=os.epoch(\"utc\")+minutes*60000});audit.log(auth.current.name,\"recovery.enable\",{minutes=minutes});return true elseif action==\"recovery_reset\"then local recovery=store.read(\"/ceetos/data/recovery.lua\",{});assert((recovery.until_ts or 0)>os.epoch(\"utc\"),\"recovery window is not active\")local name=prompt(\"User name\")if name==nil then return\"Cancelled\"end local password=prompt(\"New password\",true)if password==nil then return\"Cancelled\"end if not confirm(\"Reset password for \"..name)then return\"Cancelled\"end;auth.resetPassword(name,password);audit.log(\"local-recovery\",\"password.reset\",{name=name});return true end end)if not ok then return refresh(tostring(result),true)end if result==false then return refresh(tostring(actionError or\"Request failed\"),true)end return refresh()end local function activateSelected()local item=gui.selected(currentModel,state)if item and item.action then run(item.action)elseif item then refresh(\"This file cannot be opened\",true)end end local function nextEvent()while true do local event,first,second,third=os.pullEventRaw()if event~=\"terminate\"then return event,first,second,third end if auth.canExitOs()then if fs.exists(\"/ceetos/data\")then local marker=fs.open(\"/ceetos/data/desktop-stop\",\"w\")if marker then marker.write(\"terminate\");marker.close()end end return false end refresh(\"Administrator session required: Ctrl+T is disabled\",true)end end refresh()local uiTimer=os.startTimer(5)local nextPeerSnapshotRefresh=0 while true do local event,first,second,third=nextEvent()if event==false then return false end if state.authPending then local completed,loggedIn,loginError=auth.step(state.authPending,event,first)if completed then state.authPending=nil gui.invalidate(state)banner=loggedIn and{text=\"Signed in\",error=false}or{text=tostring(loginError or\"Auth Server did not respond\"),error=true}currentModel=gui.model(page,deps(),state)targets=gui.draw(currentModel,state,banner)event=\"ceetos_auth_completed\"end end if event==\"timer\"and first==uiTimer then uiTimer=os.startTimer((remotePending or state.pairingPending or state.authPending)and 0.2 or 5)local timestamp=os.epoch(\"utc\")local peerSnapshotDue=page==\"peers\"and state.pairingPending and timestamp>=nextPeerSnapshotRefresh if peerSnapshotDue then nextPeerSnapshotRefresh=timestamp+2000 end local pairingChanged,pairingMessage,pairingError=false,nil,false if peerSnapshotDue then pairingChanged,pairingMessage,pairingError=pollPairingMonitor()end if pollRemoteRequest()or pairingChanged or peerSnapshotDue then refresh(pairingMessage,pairingError)end elseif event==networkIpc.RESULT_EVENT then if remotePending and tostring(first or\"\")==tostring(remotePending.id)and pollRemoteRequest()then refresh()end elseif event==\"ceetos_state_changed\"and(first==\"crafting\"or first==\"crafting-state\"or first==\"/ceetos/data/crafting-state.lua\"or second==\"crafting\"or second==\"/ceetos/data/crafting-state.lua\")then if page==\"home\"then refresh()end elseif event==\"ceetos_state_changed\"then if first==\"network\"and state.pairingPending then local changed,message,isError=pollPairingMonitor()if changed then refresh(message,isError)end end if first==\"updates\"and page==\"updates\"then local update=(store.readStable and store.readStable(\"/ceetos/data/update-status.lua\",nil))or store.read(\"/ceetos/data/update-status.lua\",nil)gui.updateSnapshotChanged(state,update)refresh()elseif first==\"network\"and page==\"peers\"then refresh()end if first==\"remote\"and remotePending and pollRemoteRequest()then refresh()end elseif event==\"key\"then local rawName=tostring(keys.getName(first)or\"\"):lower()local name=input.navigation(rawName)if state.menu then if name==\"up\"then state.menu.selected=math.max(1,state.menu.selected-1);refresh()elseif name==\"down\"then state.menu.selected=math.min(#state.menu.items,state.menu.selected+1);refresh()elseif name==\"enter\"then run(state.menu.items[state.menu.selected].action)elseif name==\"backspace\"or name==\"m\"then state.menu=nil;refresh()end elseif name==\"q\"then if page==\"home\"then refresh(\"CeetOS Desktop stays active. Open Terminal for commands.\")else page=\"home\";state.selected=nil;refresh()end elseif name==\"r\"then refresh()elseif name==\"up\"or name==\"down\"or name==\"left\"or name==\"right\"then gui.move(currentModel,state,name);refresh()elseif name==\"enter\"then activateSelected()elseif name==\"m\"then openMenu(gui.selected(currentModel,state))elseif name==\"n\"and page==\"files\"and not state.trash then run(\"new_folder\")elseif name==\"t\"and page==\"files\"and not state.trash then run(\"trash_open\")elseif name==\"e\"and page==\"files\"and state.trash then run(\"empty_trash\")elseif name==\"backspace\"then if page==\"peripherals\"and state.peripheralView then state.peripheralView=false;state.selected=nil;refresh()elseif page==\"files\"and state.trash then state.trash=false;state.selected=nil;refresh()elseif page==\"files\"and state.path~=\"/\"then state.path=table.remove(state.history)or\"/\";state.selected=nil;refresh()else page=\"home\";state.selected=nil;refresh()end end elseif event==\"mouse_click\"then local target=terminalUi.hit(targets,second,third)if target then if target.kind==\"menu\"then run(target.item.action)elseif target.item.toggle then gui.select(currentModel,state,target.item);run(target.item.action)elseif first==2 then gui.select(currentModel,state,target.item);openMenu(target.item)elseif state.selected==target.item.id then if target.item.kind==\"account\"then openMenu(target.item)else activateSelected()end else gui.select(currentModel,state,target.item);refresh()end end elseif event==\"file_transfer\"then local ok,result=pcall(function()local transfers=first.getFiles();local imported,updated={},0 for _,transfer in ipairs(transfers)do local name=transfer.getName and transfer.getName()or\"\"local devBuild=name:match(\"^ceetos%-devbuild[%w%._%-]*%.lua$\")or name==\"ceetos-pastebin.lua\"local releaseBuild=name:match(\"^ceetos%-release[%w%._%-]*%.lua$\")or name==\"ceetos-release.lua\"if devBuild or releaseBuild then auth.require(\"operator\")local staged,stageErr=stageDroppedUpdate(transfer)assert(staged,stageErr)local source,sourceErr=readStagedUpdate(staged)pcall(fs.delete,staged)assert(source,sourceErr)if releaseBuild then local metadata,cacheErr=release.cache(source,\"file transfer\")assert(metadata,cacheErr or\"not a signed CeetOS release\")if confirm(\"Verified CeetOS \"..tostring(metadata.version)..\" received. Apply locally now\")then local prepared,applyErr=networkRequest(\"update_apply\",{})assert(prepared,applyErr or\"could not prepare update\")term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear();term.setCursorPos(1,1)print(\"Verified release approved. Rebooting locally...\")sleep(0.3);os.reboot()end updated=updated+1 else assert(source:find(\"CeetOS installer\",1,true)and source:find(\"local files\",1,true),\"not a CeetOS installer\")local installer=assert(load(source,name,\"t\"));installer();updated=updated+1 end else local importedFiles=files.import({transfer},\"/programs/imports\")for _,path in ipairs(importedFiles)do imported[#imported+1]=path end end end return{imported=imported,updated=updated}end)if ok then refresh()else refresh(tostring(result),true)end elseif event==\"disk\"or event==\"disk_eject\"or event==\"peripheral\"or event==\"peripheral_detach\"then if files.handleDiskEvent(event,first)then local selected=gui.selected(currentModel,state)if selected and selected.path and not fs.exists(selected.path)then state.selected=nil end refresh()end elseif event==\"term_resize\"then refresh()end end term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear();term.setCursorPos(1,1)",
  ["ceetos/bin/ceetshell.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local history=require(\"ceetos.lib.terminal\")local clipboard=require(\"ceetos.lib.clipboard\")local inputState=require(\"ceetos.lib.input\")local terminalUi=require(\"ceetos.lib.terminal_ui\")local auth=require(\"ceetos.lib.auth\")local telemetry=require(\"ceetos.lib.telemetry\")telemetry.markActivity(\"terminal\")auth.require(\"operator\")local net=require(\"ceetos.lib.net\")local store=require(\"ceetos.lib.store\")local launch={...}local remoteId,remoteLabel,remoteSession if launch[1]==\"--remote\"and launch[2]then remoteId=tostring(launch[2]);remoteLabel=\"peer-\"..remoteId for _,peer in ipairs(net.peers())do if tostring(peer.id)==remoteId and peer.label and peer.label~=\"\"then remoteLabel=peer.label end end for _,peer in ipairs(net.seenPeers())do if tostring(peer.id)==remoteId and peer.label and peer.label~=\"\"then remoteLabel=peer.label end end remoteSession=tostring(os.epoch(\"utc\"))..\"-\"..tostring(math.random(1000,9999))end local function localLabel()return(os.getComputerLabel and os.getComputerLabel())or(\"computer-\"..tostring(os.getComputerID()))end local session=(auth.refreshSession and auth.refreshSession())or auth.current or{}local promptIdentity=(session.name or\"guest\")..\"@\"..(remoteLabel or localLabel())local promptMarker=session.role==\"admin\"and\"#\"or\"$\"local promptPrefix=promptIdentity..\":~\"..promptMarker..\" \"local function parse(line)local out,token,quote,escaped={},\"\",nil,false for index=1,#line do local char=line:sub(index,index)if escaped then token,escaped=token..char,false elseif char==\"\\\\\"then escaped=true elseif quote then if char==quote then quote=nil else token=token..char end elseif char=='\"'or char==\"'\"then quote=char elseif char:match(\"%s\")then if token~=\"\"then out[#out+1],token=token,\"\"end else token=token..char end end if token~=\"\"then out[#out+1]=token end return out end local function candidates(word)local result,seen={},{}local function add(value)if value and not seen[value]then result[#result+1],seen[value]=value,true end end if word:find(\"/\")then local directory,prefix=fs.getDir(word),fs.getName(word)directory=directory==\"\"and\"/\"or directory if fs.exists(directory)and fs.isDir(directory)then for _,name in ipairs(fs.list(directory))do if name:sub(1,#prefix)==prefix then add(fs.combine(directory,name))end end end else for directory in shell.path():gmatch(\"[^:]+\")do if fs.exists(directory)and fs.isDir(directory)then for _,name in ipairs(fs.list(directory))do add(name:gsub(\"%.lua$\",\"\"))end end end end return result end local function readCommand()local input,cursor,entries,index,draft,anchor,viewOffset=\"\",0,history.history(),nil,\"\",nil,0 local held=inputState.modifiers()local selection=require(\"ceetos.lib.selection\")local promptX,promptY=1,select(2,term.getCursorPos())local function draw()local width,height=term.getSize()local prompt=promptPrefix local available=math.max(1,width-#prompt)if cursor<viewOffset then viewOffset=cursor elseif cursor>viewOffset+available then viewOffset=cursor-available end viewOffset=math.max(0,math.min(math.max(0,#input-available),viewOffset))terminalUi.clearLine(promptY,colors.black);terminalUi.write(promptX,promptY,prompt,colors.lime,colors.black,width)local visible=input:sub(viewOffset+1,viewOffset+available)local lo,hi=anchor and math.min(anchor,cursor)or nil,anchor and math.max(anchor,cursor)or nil if lo and hi and lo~=hi then local from,to=math.max(lo,viewOffset),math.min(hi,viewOffset+#visible)if from<to then terminalUi.write(#prompt+1,promptY,input:sub(viewOffset+1,from),colors.white,colors.black,available)terminalUi.write(#prompt+1+from-viewOffset,promptY,input:sub(from+1,to),colors.black,colors.white,available-(from-viewOffset))terminalUi.write(#prompt+1+to-viewOffset,promptY,input:sub(to+1,viewOffset+#visible),colors.white,colors.black,available-(to-viewOffset))else terminalUi.write(#prompt+1,promptY,visible,colors.white,colors.black,available)end term.setBackgroundColor(colors.black)else terminalUi.write(#prompt+1,promptY,visible,colors.white,colors.black,available)end term.setCursorPos(math.min(width,#prompt+cursor-viewOffset+1),promptY);if term.setCursorBlink then term.setCursorBlink(true)end terminalUi.clearLine(height,colors.black);terminalUi.write(1,height,\"Cursor \"..tostring(cursor+1)..\"/\"..tostring(#input+1)..\"  |  Enter run  |  Tab complete  |  exit returns\",colors.gray,colors.black,width)term.setTextColor(colors.white);term.setCursorPos(math.min(width,#prompt+cursor-viewOffset+1),promptY)end draw()while true do local event,first,second,third=os.pullEvent()if event==\"char\"then if held.ctrl and held.shift and(first==\"c\"or first==\"C\")and anchor then local s=require(\"ceetos.lib.selection\").new({input});require(\"ceetos.lib.selection\").start(s,1,anchor);require(\"ceetos.lib.selection\").update(s,1,cursor);require(\"ceetos.lib.selection\").copy(s,clipboard)elseif held.ctrl and held.shift and(first==\"v\"or first==\"V\")then local text=clipboard.get();input=input:sub(1,cursor)..text..input:sub(cursor+1);cursor=cursor+#text;anchor=nil else if anchor and anchor~=cursor then input=input:sub(1,math.min(anchor,cursor))..input:sub(math.max(anchor,cursor)+1);cursor=math.min(anchor,cursor);anchor=nil end;input=input:sub(1,cursor)..first..input:sub(cursor+1);cursor=cursor+#first end;draw()elseif event==\"paste\"then if anchor and anchor~=cursor then input=input:sub(1,math.min(anchor,cursor))..input:sub(math.max(anchor,cursor)+1);cursor=math.min(anchor,cursor);anchor=nil end;input=input:sub(1,cursor)..first..input:sub(cursor+1);cursor=cursor+#first;draw()elseif event==\"key_down\"then inputState.key(held,keys.getName(first)or first)elseif event==\"key_up\"then inputState.keyUp(held,keys.getName(first)or first)elseif event==\"key\"then local key=tostring(keys.getName(first)or first or\"\"):lower()local copyShortcut=held.ctrl and held.shift and key==\"c\"local pasteShortcut=held.ctrl and held.shift and key==\"v\"if key==\"leftctrl\"or key==\"rightctrl\"or key==\"ctrl\"or key==\"leftshift\"or key==\"rightshift\"or key==\"shift\"then inputState.key(held,key);draw()elseif copyShortcut then held.ctrl,held.shift=false,false if anchor and anchor~=cursor then local s=selection.new({input});selection.start(s,1,anchor);selection.update(s,1,cursor);selection.copy(s,clipboard)end;draw()elseif pasteShortcut then held.ctrl,held.shift=false,false local text=clipboard.get();if anchor and anchor~=cursor then input=input:sub(1,math.min(anchor,cursor))..input:sub(math.max(anchor,cursor)+1);cursor=math.min(anchor,cursor);anchor=nil end;input=input:sub(1,cursor)..text..input:sub(cursor+1);cursor=cursor+#text;draw()elseif key==\"enter\"then term.setCursorPos(1,promptY+1);return input elseif key==\"backspace\"then if anchor and anchor~=cursor then input=input:sub(1,math.min(anchor,cursor))..input:sub(math.max(anchor,cursor)+1);cursor=math.min(anchor,cursor);anchor=nil elseif cursor>0 then input=input:sub(1,cursor-1)..input:sub(cursor+1);cursor=cursor-1 end;draw()elseif key==\"delete\"and cursor<#input then if anchor and anchor~=cursor then input=input:sub(1,math.min(anchor,cursor))..input:sub(math.max(anchor,cursor)+1);cursor=math.min(anchor,cursor);anchor=nil else input=input:sub(1,cursor)..input:sub(cursor+2)end;draw()elseif key==\"left\"then if held.shift then anchor=anchor or cursor end;cursor=math.max(0,cursor-1);if not held.shift then anchor=nil end;draw()elseif key==\"right\"then if held.shift then anchor=anchor or cursor end;cursor=math.min(#input,cursor+1);if not held.shift then anchor=nil end;draw()elseif key==\"home\"then if held.shift then anchor=anchor or cursor end;cursor=0;if not held.shift then anchor=nil end;draw()elseif key==\"end\"then if held.shift then anchor=anchor or cursor end;cursor=#input;if not held.shift then anchor=nil end;draw()elseif key==\"up\"then if#entries>0 then if not index then draft,index=input,#entries else index=math.max(1,index-1)end;input,cursor=entries[index],#entries[index];draw()end elseif key==\"down\"and index then index=index+1;input=index>#entries and draft or entries[index];if index>#entries then index=nil end;cursor=#input;draw()elseif key==\"tab\"then local word=input:match(\"([^%s]*)$\")or\"\";local matches=history.complete(word,candidates(word))if#matches==1 then input=input:sub(1,#input-#word)..matches[1];cursor=#input;draw()end end elseif event==\"mouse_click\"and first==2 and anchor and anchor~=cursor then local s=selection.new({input});selection.start(s,1,anchor);selection.update(s,1,cursor);selection.copy(s,clipboard);draw()elseif event==\"mouse_click\"and first==1 and third==promptY then local prompt=promptPrefix;anchor=math.max(0,math.min(#input,viewOffset+second-#prompt-1));cursor=anchor;draw()elseif event==\"mouse_drag\"and first==1 and third==promptY then local prompt=promptPrefix;cursor=math.max(0,math.min(#input,viewOffset+second-#prompt-1));draw()end end end local function banner()local width=select(1,term.getSize())term.setBackgroundColor(colors.black);term.setTextColor(colors.cyan);term.clear();terminalUi.write(1,1,\"CeetOS Terminal  |  \"..promptIdentity..\"  |  Tab complete  |  exit returns to Desktop\",colors.cyan,colors.black,width)term.setCursorPos(1,3)term.setTextColor(colors.white)end local function waitRemote()if not remoteSession then return end local timer,deadline=os.startTimer(0.2),os.epoch(\"utc\")+2000 while os.epoch(\"utc\")<deadline do local event,value=os.pullEvent()if event==\"timer\"and value==timer then local replies,pending=store.read(\"/ceetos/data/shell-replies.lua\",{}),{}for _,reply in ipairs(replies)do if reply.from==remoteId and reply.body and reply.body.session==remoteSession then if reply.body.output and reply.body.output~=\"\"then print(reply.body.output)end if not reply.body.ok and reply.body.error then printError(reply.body.error)end else pending[#pending+1]=reply end end store.write(\"/ceetos/data/shell-replies.lua\",pending)if#pending<#replies then return end timer=os.startTimer(0.2)end end printError(\"Remote command timed out\")end local function configureDevBridge(url,token)if not url or url==\"\"then printError(\"usage: ceetdev <ws://bridge:port/dev> [token]\");return end if type(url)~=\"string\"or not url:match(\"^wss?://[^/%s]+/dev$\")or#url>256 then printError(\"invalid development bridge URL\");return false end local allowed,err=pcall(auth.require,\"admin\")if not allowed then printError(tostring(err));return false end local function writeValue(path,value)local parent,temporary=fs.getDir(path),path..\".tmp\"if parent~=\"\"and not fs.exists(parent)then fs.makeDir(parent)end if fs.exists(temporary)then fs.delete(temporary)end local handle=assert(fs.open(temporary,\"w\"));handle.write(textutils.serialise(value));handle.close()if fs.exists(path)then fs.delete(path)end fs.move(temporary,path)end writeValue(\"/ceetos-dev/connection.lua\",{url=url,token=token or\"\",auth=(token and token~=\"\")and\"token\"or\"none\",source=\"terminal\"})writeValue(\"/ceetos-dev/recovery/developer-mode.lua\",{schema=1,enabledAt=os.epoch(\"utc\"),untilAt=os.epoch(\"utc\")+8*60*60*1000,source=\"terminal\"})print(\"Development bridge configured for 8 hours; connection continues in the background.\")return true end banner()while true do local line=readCommand()if line==nil or line==\"exit\"then break end if line==\"clear\"then banner()else local args=parse(line)if#args>0 then history.add(line)if remoteId then local ok,err=net.request(remoteId,\"shell_request\",{command=line,session=remoteSession})if not ok then printError(err)else waitRemote()end else local ok,result if args[1]==\"ceetdev\"then ok,result=configureDevBridge(args[2],args[3]),true else ok,result=pcall(shell.run,args[1],table.unpack(args,2))end if not ok then printError(result)elseif result==false then printError(\"Command failed\")end end end end end",
  ["ceetos/bin/ceetlaunch.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local auth=require(\"ceetos.lib.auth\")local capabilities=require(\"ceetos.lib.program_capabilities\")local disks=require(\"ceetos.lib.disks\")local audit=require(\"ceetos.lib.audit\")local telemetry=require(\"ceetos.lib.telemetry\")local args={...}local requestedPath=table.remove(args,1)assert(requestedPath and requestedPath~=\"\",\"usage: ceetlaunch <program> [args]\")if auth.refreshSession then auth.refreshSession()end local current=auth.current local actor=current and current.name or\"anonymous\"local normalized,pathErr=capabilities.normaliseProgramPath(requestedPath)if not normalized then audit.log(actor,\"program.launch_denied\",{path=tostring(requestedPath),reason=tostring(pathErr)})error(pathErr,0)end telemetry.markActivity(\"program: \"..(fs.getName(normalized)or\"program\"))local runner local removable=disks.forPath(normalized)if removable then local trusted,record=capabilities.hasPortableTrust(normalized,removable.token)if trusted and auth.allowed(\"admin\")then audit.log(actor,\"program.launch\",{path=normalized,mode=\"portable-temporary-trust\",volume=removable.key})runner=function()return shell.run(normalized,table.unpack(args))end else local chunk,sandboxRecord=capabilities.loadViewer(normalized,actor)if not chunk then audit.log(actor,\"program.launch_denied\",{path=normalized,reason=tostring(sandboxRecord),removable=removable.key})error(sandboxRecord,0)end audit.log(actor,\"program.launch\",{path=normalized,mode=\"portable-sandbox\",volume=removable.key,trust=sandboxRecord.trust,stale=sandboxRecord.stale==true})runner=function()return chunk(table.unpack(args))end end elseif auth.allowed(\"operator\")then audit.log(actor,\"program.launch\",{path=normalized,mode=\"full\",role=auth.current and auth.current.role})runner=function()return shell.run(normalized,table.unpack(args))end elseif auth.allowed(\"viewer\")then local chunk,record=capabilities.loadViewer(normalized,actor)if not chunk then audit.log(actor,\"program.launch_denied\",{path=normalized,reason=tostring(record)})error(record,0)end audit.log(actor,\"program.launch\",{path=normalized,mode=\"sandbox\",trust=record.trust,stale=record.stale==true,})runner=function()return chunk(table.unpack(args))end else audit.log(actor,\"program.launch_denied\",{path=normalized,reason=\"requires viewer\"})error(\"permission denied (requires viewer)\",0)end local child=coroutine.create(runner)local function resume(...)local ok,result=coroutine.resume(child,...)if not ok then audit.log(actor,\"program.failed\",{path=normalized,reason=\"program error\"})error(result,0)end return coroutine.status(child)==\"dead\",result end local done,result=resume()while not done do local event={os.pullEventRaw()}if event[1]==\"key\"and event[2]==keys.backspace then term.setTextColor(colors.gray)print(\"Backspace: returned to CeetOS Desktop\")return true end done,result=resume(table.unpack(event))end return result",
  ["ceetos/bin/craftos-shell.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local auth=require(\"ceetos.lib.auth\")auth.require(\"operator\")shell.run(\"/rom/programs/shell.lua\")",
  ["ceetos/bin/ceetedit.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local editor=require(\"ceetos.lib.editor\")local clipboard=require(\"ceetos.lib.clipboard\")local inputState=require(\"ceetos.lib.input\")local terminalUi=require(\"ceetos.lib.terminal_ui\")local auth=require(\"ceetos.lib.auth\")local files=require(\"ceetos.lib.files\")local args={...}local path=args[1]assert(path and path~=\"\",\"usage: ceetedit <file>\")auth.require(\"operator\")assert(not files.isProtected(path),\"protected files must be edited with craftos-edit\")local buffer=editor.new(fs.exists(path)and files.readText(path)or\"\")local scroll,ctrl,shift=1,false,false local held=inputState.modifiers()local tokenColours={text=colors.white,comment=colors.gray,string=colors.orange,keyword=colors.purple,number=colors.cyan,builtin=colors.lightBlue}local function fitCursor(height)local visible=math.max(1,height-3)if buffer.line<scroll then scroll=buffer.line elseif buffer.line>=scroll+visible then scroll=buffer.line-visible+1 end end local function render(message)local width,height=term.getSize();fitCursor(height)term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear()if width<24 or height<8 then terminalUi.minimum(\"CeetOS Editor\",\"Editor needs 24x8\",\"Resize | Ctrl+Q exit\");return end terminalUi.clearLine(1,colors.blue);terminalUi.write(2,1,\"CeetOS Editor | \"..path..(buffer.changed and\" *\"or\"\"),colors.white,colors.blue,width-2)local digits=math.max(2,#tostring(#buffer.lines));local gutter=digits+3 local lua=path:sub(-4):lower()==\".lua\"for row=2,height-1 do local lineNumber,line=scroll+row-2,buffer.lines[scroll+row-2]if line then local selected=lineNumber==buffer.line term.setBackgroundColor(selected and colors.gray or colors.black);terminalUi.write(1,row,(\"%\"..digits..\"d \"):format(lineNumber),colors.lightGray,selected and colors.gray or colors.black,gutter-1)term.setTextColor(colors.white)local x=gutter for _,token in ipairs(editor.tokens(line,lua))do if x<width then terminalUi.write(x,row,token.text,tokenColours[token.kind]or colors.white,selected and colors.gray or colors.black,width-x+1);x=x+#token.text end end end end local range=buffer.selection and require(\"ceetos.lib.selection\").range(buffer.selection)if range then for line=range.first.line,range.last.line do local row=line-scroll+2 if row>=2 and row<height then local from=line==range.first.line and range.first.column or 0 local to=line==range.last.line and range.last.column or#(buffer.lines[line]or\"\")if to>from and gutter+from<=width then terminalUi.write(gutter+from,row,(buffer.lines[line]or\"\"):sub(from+1,to),colors.black,colors.white,width-gutter-from+1)term.setBackgroundColor(colors.black)end end end end terminalUi.clearLine(height,colors.black);terminalUi.write(1,height,message or\"Ctrl+S save | F2 save as | Ctrl+F find | Ctrl+G line | F5 run | Ctrl+Q exit\",colors.gray,colors.black,width)local cursorY=buffer.line-scroll+2 if cursorY>=2 and cursorY<height then term.setCursorPos(math.min(width,gutter+buffer.column),cursorY)end term.setTextColor(colors.white)end local function prompt(label)local _,height=term.getSize();term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.setCursorPos(1,height);term.clearLine();term.write(label..\": \");return read()end local function save(target)target=target or path local ok,err=pcall(files.writeText,target,editor.text(buffer))if ok then path,buffer.changed=target,false;return true else render(tostring(err));return false end end local function leaveChoice()if not buffer.changed then return\"discard\"end local function draw()render()local width,height=term.getSize();local boxWidth=math.max(20,math.min(32,width-2));local x,y=math.max(1,math.floor((width-boxWidth)/2)+1),math.max(2,math.floor((height-5)/2))terminalUi.fill(x,y,boxWidth,5,colors.black)terminalUi.write(x+2,y+1,\"Unsaved changes\",colors.white,colors.black,boxWidth-4)local compact=boxWidth<31 local saveText,discardText,cancelText=compact and\"[S]\"or\" Save \",compact and\"[D]\"or\" Discard \",compact and\"[C]\"or\" Cancel \"local saveX,discardX,cancelX=x+2,x+2+#saveText+2,x+2+#saveText+#discardText+4 terminalUi.write(saveX,y+3,saveText,colors.black,colors.green)terminalUi.write(discardX,y+3,discardText,colors.white,colors.red)terminalUi.write(cancelX,y+3,cancelText,colors.white,colors.gray)return{y=y+3,saveX=saveX,saveWidth=#saveText,discardX=discardX,discardWidth=#discardText,cancelX=cancelX,cancelWidth=#cancelText}end local modal=draw()while true do local event,first,second,third=os.pullEvent()if event==\"mouse_click\"and third==modal.y then if second>=modal.saveX and second<modal.saveX+modal.saveWidth then return\"save\"elseif second>=modal.discardX and second<modal.discardX+modal.discardWidth then return\"discard\"elseif second>=modal.cancelX and second<modal.cancelX+modal.cancelWidth then return\"cancel\"end elseif event==\"key\"then local key=keys.getName(first);if key==\"enter\"then return\"save\"elseif key==\"d\"then return\"discard\"elseif key==\"backspace\"then return\"cancel\"end end if event==\"term_resize\"then modal=draw()end end end render()while true do local event,first,second,third=os.pullEvent()if event==\"char\"then editor.insert(buffer,first);render()elseif event==\"paste\"then editor.insert(buffer,first);render()elseif event==\"key_up\"then local released=tostring(keys.getName(first)or first or\"\"):lower()inputState.keyUp(held,released);ctrl,shift=held.ctrl,held.shift elseif event==\"key\"then local key=tostring(keys.getName(first)or first or\"\"):lower()if key==\"leftctrl\"or key==\"rightctrl\"or key==\"ctrl\"or key==\"leftshift\"or key==\"rightshift\"or key==\"shift\"then inputState.key(held,key);ctrl,shift=held.ctrl,held.shift elseif ctrl and key==\"c\"then editor.copySelection(buffer,clipboard);ctrl,shift,held.ctrl,held.shift=false,false,false,false;render(\"Copied\")elseif ctrl and key==\"v\"then editor.insert(buffer,clipboard.get());ctrl,shift,held.ctrl,held.shift=false,false,false,false;render()elseif ctrl and key==\"s\"then save();render(\"Saved\")elseif ctrl and key==\"q\"then local choice=leaveChoice();if choice==\"save\"then if save()then break end elseif choice==\"discard\"then break else render()end end elseif ctrl and key==\"f\"then local needle=prompt(\"Find\");if not editor.find(buffer,needle)then render(\"Not found\")else render()end elseif ctrl and key==\"g\"then editor.gotoLine(buffer,prompt(\"Go to line\"));render()elseif key==\"f2\"then local target=prompt(\"Save as\");if target~=\"\"then save(target);render(\"Saved\")else render()end elseif key==\"f5\"then if not auth.allowed(\"operator\")then render(\"Operator role required to run programs\")elseif save()then term.clear();term.setCursorPos(1,1);shell.run(\"/ceetos/bin/ceetlaunch.lua\",path);render(\"Returned from program\")end elseif key==\"left\"then if shift and not buffer.selection.anchor then editor.selectStart(buffer)end;editor.move(buffer,0,-1);if shift then editor.selectTo(buffer,buffer.line,buffer.column)else editor.clearSelection(buffer)end;render()elseif key==\"right\"then if shift and not buffer.selection.anchor then editor.selectStart(buffer)end;editor.move(buffer,0,1);if shift then editor.selectTo(buffer,buffer.line,buffer.column)else editor.clearSelection(buffer)end;render()elseif key==\"up\"then if shift and not buffer.selection.anchor then editor.selectStart(buffer)end;editor.move(buffer,-1,0);if shift then editor.selectTo(buffer,buffer.line,buffer.column)else editor.clearSelection(buffer)end;render()elseif key==\"down\"then if shift and not buffer.selection.anchor then editor.selectStart(buffer)end;editor.move(buffer,1,0);if shift then editor.selectTo(buffer,buffer.line,buffer.column)else editor.clearSelection(buffer)end;render()elseif key==\"home\"then if shift and not buffer.selection.anchor then editor.selectStart(buffer)end;editor.gotoLine(buffer,buffer.line,0);if shift then editor.selectTo(buffer,buffer.line,buffer.column)else editor.clearSelection(buffer)end;render()elseif key==\"end\"then if shift and not buffer.selection.anchor then editor.selectStart(buffer)end;editor.gotoLine(buffer,buffer.line,#editor.current(buffer));if shift then editor.selectTo(buffer,buffer.line,buffer.column)else editor.clearSelection(buffer)end;render()elseif key==\"pageup\"then editor.move(buffer,-10,0);render()elseif key==\"pagedown\"then editor.move(buffer,10,0);render()elseif key==\"enter\"then editor.insert(buffer,\"\\n\");render()elseif key==\"tab\"then editor.insert(buffer,\"  \");render()elseif key==\"backspace\"then if not editor.deleteSelection(buffer)then editor.backspace(buffer)end;render()elseif key==\"delete\"then if not editor.deleteSelection(buffer)then editor.delete(buffer)end;render()elseif event==\"mouse_click\"and first==2 then if buffer.selection and require(\"ceetos.lib.selection\").range(buffer.selection)then editor.copySelection(buffer,clipboard);render(\"Copied\")else editor.insert(buffer,clipboard.get());render()end elseif event==\"mouse_click\"and first==1 then local width,height=term.getSize()local digits=math.max(2,#tostring(#buffer.lines));local gutter=digits+3 local line=scroll+third-2 if third>=2 and third<height and line>=1 and line<=#buffer.lines then local column=math.max(0,math.min(#(buffer.lines[line]or\"\"),second-gutter))editor.gotoLine(buffer,line,column);editor.selectStart(buffer);render()end elseif event==\"mouse_drag\"and first==1 then local width,height=term.getSize()local digits=math.max(2,#tostring(#buffer.lines));local gutter=digits+3 local line=math.max(1,math.min(#buffer.lines,scroll+third-2))local column=math.max(0,math.min(#(buffer.lines[line]or\"\"),second-gutter))editor.selectTo(buffer,line,column);editor.gotoLine(buffer,line,column);render()elseif event==\"term_resize\"then render()end end term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear();term.setCursorPos(1,1)",
  ["ceetos/bin/edit.lua"] = "local args={...}if#args==0 then shell.run(\"/ceetos/bin/ceetui.lua\",\"files\")else shell.run(\"/ceetos/bin/ceetedit.lua\",args[1])end",
  ["ceetos/bin/programs.lua"] = "shell.run(\"/ceetos/bin/ceetui.lua\")",
  ["ceetos/bin/files.lua"] = "shell.run(\"/ceetos/bin/ceetui.lua\",\"files\")",
  ["ceetos/bin/list.lua"] = "shell.run(\"/ceetos/bin/ceetui.lua\",\"files\")",
  ["ceetos/bin/delete.lua"] = "shell.run(\"/ceetos/bin/ceetui.lua\",\"files\")",
  ["ceetos/lib/store.lua"] = "local M={}local cache={}local function internalPath(path)return type(path)~=\"string\"or path:sub(-4)==\".new\"or path:sub(-4)==\".bak\"end local function call(fn,...)local result={pcall(fn,...)}if not result[1]then return false,tostring(result[2])end if result[2]==false then return false,tostring(result[3]or\"operation failed\")end return true,result[2],result[3]end local function exists(path)local ok,value=call(fs.exists,path)return ok and value==true end local function remove(path)if not exists(path)then return true end local ok,err=call(fs.delete,path)return ok,err end local function ensureDirectory(path)local dir=fs.getDir(path)if dir==\"\"or exists(dir)then return true end local ok,err=call(fs.makeDir,dir)return ok,err end local function readRaw(path)local ok,handleOrErr=call(fs.open,path,\"r\")if not ok or not handleOrErr then return false,nil,tostring(handleOrErr or\"could not open file\")end local handle=handleOrErr local readOk,content=call(handle.readAll)local closeOk,closeErr=call(handle.close)if not readOk then return false,nil,content end if not closeOk then return false,nil,closeErr end return true,content end local function writeRaw(path,content)local ok,handleOrErr=call(fs.open,path,\"w\")if not ok or not handleOrErr then return false,tostring(handleOrErr or\"could not open file\")end local handle=handleOrErr local writeOk,writeErr=call(handle.write,content)local closeOk,closeErr=call(handle.close)if not writeOk then return false,writeErr end if not closeOk then return false,closeErr end return true end local function move(source,destination)local ok,err=call(fs.move,source,destination)return ok,err end local function decode(raw)local ok,value=pcall(textutils.unserialise,raw)if not ok or value==nil then return false,nil,\"invalid serialised value\"end return true,value end local function readValue(path)local ok,raw,err=readRaw(path)if not ok then return false,nil,err end return decode(raw)end local function recoverValue(path)local activeOk,activeValue=false,nil if exists(path)then activeOk,activeValue=readValue(path)end if activeOk then remove(path..\".new\")remove(path..\".bak\")return true,activeValue end local backupOk,backupValue=false,nil if exists(path..\".bak\")then backupOk,backupValue=readValue(path..\".bak\")end if backupOk then if exists(path)then remove(path)end if not exists(path)then move(path..\".bak\",path)end remove(path..\".new\")return true,backupValue end remove(path..\".new\")return false,nil end local function validLines(raw)for line in(raw..\"\\n\"):gmatch(\"(.-)\\n\")do if line~=\"\"then local ok,value=pcall(textutils.unserialise,line)if not ok or value==nil then return false,\"invalid appended value\"end end end return true end local function recoverAppend(path)if exists(path)then local activeOk,raw=readRaw(path)if activeOk then local valid=validLines(raw)if valid then remove(path..\".new\")remove(path..\".bak\")return true,raw end end end if exists(path..\".bak\")then local backupOk,raw=readRaw(path..\".bak\")if backupOk and validLines(raw)then if exists(path)then remove(path)end if not exists(path)then move(path..\".bak\",path)end remove(path..\".new\")return true,raw end end remove(path..\".new\")return false,\"no valid appended value\"end local function commitRaw(path,raw,validator)local dirOk,dirErr=ensureDirectory(path)if not dirOk then return false,dirErr end local temporary,backup=path..\".new\",path..\".bak\"local removeOk,removeErr=remove(temporary)if not removeOk then return false,removeErr end local writeOk,writeErr=writeRaw(temporary,raw)if not writeOk then remove(temporary);return false,writeErr end local checkOk,checkedRaw,checkErr=readRaw(temporary)if not checkOk then remove(temporary);return false,checkErr end local valid,validErr=validator(checkedRaw)if not valid then remove(temporary);return false,validErr or\"temporary validation failed\"end local backupRemoveOk,backupRemoveErr=remove(backup)if not backupRemoveOk then remove(temporary);return false,backupRemoveErr end if exists(path)then local backupOk,backupErr=move(path,backup)if not backupOk then remove(temporary);return false,backupErr end local activateOk,activateErr=move(temporary,path)if not activateOk then if not exists(path)then move(backup,path)end remove(temporary)return false,activateErr end else local activateOk,activateErr=move(temporary,path)if not activateOk then remove(temporary);return false,activateErr end end return true end function M.readCached(path,fallback)if internalPath(path)then return fallback end local entry=cache[path]if entry~=nil then return entry.value end return M.readFresh(path,fallback)end function M.readFresh(path,fallback)if internalPath(path)then return fallback end local ok,value=recoverValue(path)if not ok then value=fallback end cache[path]={value=value}return value end function M.readStable(path,fallback)if internalPath(path)then return fallback end local ok,value=readValue(path)if not ok then return fallback end return value end function M.read(path,fallback)return M.readCached(path,fallback)end function M.writeAtomic(path,value)if internalPath(path)then return false,\"cannot write store temporary or backup path\"end if value==nil then return false,\"cannot store nil\"end local serialisedOk,raw=pcall(textutils.serialise,value,{compact=true})if not serialisedOk or type(raw)~=\"string\"then return false,tostring(raw or\"could not serialise value\")end local valid=decode(raw)if not valid then return false,\"could not validate serialised value\"end recoverValue(path)local ok,err=commitRaw(path,raw,decode)if ok then cache[path]={value=value}end return ok,err end function M.write(path,value)return M.writeAtomic(path,value)end function M.append(path,value,limits)if internalPath(path)then return false,\"cannot append to store temporary or backup path\"end if value==nil then return false,\"cannot append nil\"end local serialisedOk,line=pcall(textutils.serialise,value,{compact=true})if not serialisedOk or type(line)~=\"string\"then return false,tostring(line or\"could not serialise value\")end local valid=validLines(line..\"\\n\")if not valid then return false,\"could not validate appended value\"end local recovered,current=recoverAppend(path)if not recovered then if exists(path)or exists(path..\".bak\")then return false,current end current=\"\"end local lines={}for existing in(current..\"\\n\"):gmatch(\"(.-)\\n\")do if existing~=\"\"then lines[#lines+1]=existing end end lines[#lines+1]=line local maxEntries=limits and tonumber(limits.maxEntries)local maxBytes=limits and tonumber(limits.maxBytes)if maxEntries then maxEntries=math.max(1,math.floor(maxEntries))end if maxBytes then maxBytes=math.max(1,math.floor(maxBytes))end while maxEntries and#lines>maxEntries do table.remove(lines,1)end local raw=table.concat(lines,\"\\n\")if#raw>0 then raw=raw..\"\\n\"end while maxBytes and#raw>maxBytes and#lines>1 do table.remove(lines,1)raw=table.concat(lines,\"\\n\")..\"\\n\"end if maxBytes and#raw>maxBytes then return false,\"appended value exceeds byte limit\"end local ok,err=commitRaw(path,raw,validLines)if ok then cache[path]=nil end return ok,err end function M.readAppend(path)if internalPath(path)then return\"\"end local ok,raw=recoverAppend(path)return ok and raw or\"\"end function M.invalidate(path)cache[path]=nil end return M",
  ["ceetos/lib/runtime_modules.lua"] = "local M={}function M.refresh()local loaded=package and package.loaded if type(loaded)~=\"table\"then return 0 end local names={}for name in pairs(loaded)do if type(name)==\"string\"and name:sub(1,7)==\"ceetos.\"then names[#names+1]=name end end for _,name in ipairs(names)do loaded[name]=nil end return#names end return M",
  ["ceetos/lib/supervisor.lua"] = "local M={}local Supervisor={}Supervisor.__index=Supervisor local function clamp(value,minimum,maximum)value=tonumber(value)or minimum if value<minimum then return minimum end if value>maximum then return maximum end return value end local function bounded(value,maximum)value=tostring(value or\"worker returned\")return value:sub(1,maximum or 240)end function M.new(options)options=options or{}assert(type(options.now)==\"function\",\"supervisor requires a clock\")assert(type(options.sleep)==\"function\",\"supervisor requires a sleep function\")return setmetatable({now=options.now,sleep=options.sleep,record=type(options.record)==\"function\"and options.record or function()end,baseDelay=clamp(options.baseDelay,0.1,60),maxDelay=clamp(options.maxDelay,0.1,300),failures={},},Supervisor)end function Supervisor:attempt(name,worker)assert(type(name)==\"string\"and#name>0 and#name<=48,\"invalid worker name\")assert(type(worker)==\"function\",\"worker must be a function\")local previous=self.failures[name]or 0 pcall(self.record,name,{state=\"running\",failures=previous,at=self.now()})local ok,value=xpcall(worker,function(errorText)return bounded(errorText)end)local failures=previous+1 local delay=math.min(self.maxDelay,self.baseDelay*(2^math.min(failures-1,10)))failures=math.min(failures,1024)self.failures[name]=failures local status={state=\"backoff\",failures=failures,delay=delay,error=ok and\"worker returned\"or bounded(value),at=self.now(),}pcall(self.record,name,status)return false,status end function Supervisor:run(name,worker)while true do local _,status=self:attempt(name,worker)self.sleep(status.delay)end end function Supervisor:status(name)return self.failures[name]or 0 end return M",
  ["ceetos/lib/profile.lua"] = "local store=require(\"ceetos.lib.store\")local M={}M.PATH=\"/ceetos/data/profile.lua\"M.INSTALLED_PATH=\"/ceetos/profile.lua\"M.IDS={desktop=true,[\"recipe-server\"]=true,[\"router-server\"]=true,[\"auth-server\"]=true}local cache local function normalise(value)if type(value)~=\"table\"then value={}end local id=tostring(value.id or value.profile or\"desktop\")if not M.IDS[id]then id=\"desktop\"end return{schema=1,id=id,installedAt=tonumber(value.installedAt)or 0}end function M.parse(raw)if type(raw)~=\"string\"or#raw>1024 then return nil,\"invalid profile data\"end local ok,value=pcall(textutils.unserialise,raw)if ok and type(value)==\"table\"then return normalise(value)end local schema,id=raw:match('^%s*return%s*%{%s*schema%s*=%s*(%d+)%s*,%s*id%s*=%s*[\"\\']([%w%-]+)[\"\\']%s*,?%s*%}%s*$')if schema and M.IDS[id]then return normalise({schema=tonumber(schema),id=id})end return nil,\"invalid profile data\"end local function readPath(path)if not fs.exists(path)then return nil end local handle=fs.open(path,\"r\")if not handle then return normalise({id=\"desktop\"})end local raw=handle.readAll()handle.close()return M.parse(raw)end local function readCurrent()return readPath(M.PATH)or readPath(M.INSTALLED_PATH)or normalise({id=\"desktop\"})end function M.current()if not cache then cache=readCurrent()end return cache end function M.id()return M.current().id end function M.is(id)return M.id()==id end function M.isServer()return M.id()~=\"desktop\"end function M.set(id)assert(M.IDS[id],\"invalid CeetOS profile\")cache={schema=1,id=id,installedAt=os.epoch(\"utc\")}return store.writeAtomic(M.PATH,cache)end function M.capability(name)local id=M.id()local tableFor={desktop={desktop=true,craftingClient=true,orders=true},[\"recipe-server\"]={server=true,recipes=true,recipeImport=true,recipePlan=true},[\"router-server\"]={server=true,routing=true},[\"auth-server\"]={server=true,authAuthority=true},}return tableFor[id][name]==true end return M",
  ["ceetos/lib/auth.lua"] = "local store=require(\"ceetos.lib.store\")local password=require(\"ceetos.lib.password\")local audit=require(\"ceetos.lib.audit\")local M={current=nil}local USERS=\"/ceetos/data/users.lua\"local SESSION=\"/ceetos/data/session.lua\"local TOMBSTONES=\"/ceetos/data/user-tombstones.lua\"local CONFLICTS=\"/ceetos/data/account-conflicts.lua\"local THROTTLE=\"/ceetos/data/login-throttle.lua\"local RECOVERY=\"/ceetos/data/recovery.lua\"local AUTHORITY=\"/ceetos/data/auth-authority.lua\"local CENTRAL_SESSION=\"/ceetos/data/auth-session.lua\"local DIRECTORY=\"/ceetos/data/auth-directory.lua\"local rank={viewer=1,operator=2,admin=3}M.SESSION_TTL_MS=8*60*60*1000 M.LOGIN_THROTTLE={failures=5,windowMs=5*60*1000,lockMs=60*1000,maxEntries=64}local configuredWorkFactor=password.DEFAULT_WORK_FACTOR local sessionPathDelete local function now()return os.epoch(\"utc\")end local function accountName(name)return type(name)==\"string\"and name:match(\"^[%w_%-]+$\")and#name<=32 end local function users()store.invalidate(USERS);return store.read(USERS,{})end local function save(value)return store.write(USERS,value)end local function tombstones()store.invalidate(TOMBSTONES);return store.read(TOMBSTONES,{})end local function saveTombstones(value)return store.write(TOMBSTONES,value)end local function conflicts()store.invalidate(CONFLICTS)local value=store.read(CONFLICTS,{})return type(value)==\"table\"and value or{}end local function saveConflicts(value)return store.write(CONFLICTS,value)end local function sameSecurityRecord(left,right)if type(left)~=\"table\"or type(right)~=\"table\"then return false end return left.role==right.role and left.salt==right.salt and left.verifier==right.verifier and left.passwordVersion==right.passwordVersion and left.scheme==right.scheme and tonumber(left.workFactor)==tonumber(right.workFactor)and(left.disabled==true)==(right.disabled==true)end local function recordConflict(name,localAccount,incoming,localOrigin,incomingOrigin)local entries=conflicts()local previous=type(entries[name])==\"table\"and entries[name]or{}entries[name]={name=name,detected=now(),occurrences=math.min(999,math.max(0,math.floor(tonumber(previous.occurrences)or 0))+1),localOrigin=tostring(localOrigin):sub(1,64),incomingOrigin=tostring(incomingOrigin):sub(1,64),localRole=tostring(localAccount.role),incomingRole=tostring(incoming.role),localDisabled=localAccount.disabled==true,incomingDisabled=incoming.disabled==true,localRevision=math.max(0,math.floor(tonumber(localAccount.revision)or 0)),incomingRevision=math.max(0,math.floor(tonumber(incoming.revision)or 0)),localUpdated=math.max(0,math.floor(tonumber(localAccount.updated)or 0)),incomingUpdated=math.max(0,math.floor(tonumber(incoming.updated)or 0)),}local ordered={}for entryName,entry in pairs(entries)do if accountName(entryName)and type(entry)==\"table\"then ordered[#ordered+1]={name=entryName,at=tonumber(entry.detected)or 0}end end table.sort(ordered,function(a,b)return a.at>b.at end)for index=33,#ordered do entries[ordered[index].name]=nil end return saveConflicts(entries)end local function authority()store.invalidate(AUTHORITY)local value=store.read(AUTHORITY,nil)return type(value)==\"table\"and value.active==true and type(value.id)==\"string\"and value or nil end function M.authority()return authority()end function M.centralActive()return authority()~=nil end local function centralDirectory()store.invalidate(DIRECTORY)local value=store.read(DIRECTORY,{})return type(value)==\"table\"and type(value.users)==\"table\"and value or{users={}}end function M.acceptAuthority(record,context)assert(type(record)==\"table\"and type(record.id)==\"string\"and#record.id<=64,\"invalid auth authority\")assert(type(context)==\"table\"and context.direct==true and tostring(context.from)==record.id,\"untrusted auth authority advertisement\")local incoming={schema=1,active=true,id=record.id,term=math.max(0,math.floor(tonumber(record.term)or 0)),revision=math.max(0,math.floor(tonumber(record.revision)or 0)),seen=now(),}local current=store.readFresh(AUTHORITY,nil)assert(type(current)==\"table\"and current.active==true,\"new auth authority requires local administrator enrollment\")assert(incoming.id==current.id,\"auth authority identity is pinned\")if type(current)==\"table\"and current.active==true then local currentTerm,currentRevision=math.max(0,math.floor(tonumber(current.term)or 0)),math.max(0,math.floor(tonumber(current.revision)or 0))if incoming.term<currentTerm or(incoming.term==currentTerm and incoming.id~=current.id)or(incoming.term==currentTerm and incoming.id==current.id and incoming.revision<currentRevision)then current.seen=incoming.seen assert(store.write(AUTHORITY,current),\"could not refresh auth authority\")return current,\"stale authority advertisement ignored\"end end assert(store.write(AUTHORITY,incoming),\"could not save auth authority\")return incoming end function M.acceptCloudAuthority(record,endpoint)assert(type(record)==\"table\"and type(record.id)==\"string\",\"invalid cloud auth authority\")assert(type(endpoint)==\"string\"and endpoint:match(\"^https://\"),\"invalid cloud auth endpoint\")assert(record.id==\"cloud:\"..endpoint,\"cloud authority endpoint mismatch\")local current=store.readFresh(AUTHORITY,nil)if type(current)==\"table\"and current.active==true and current.id~=record.id then error(\"auth authority identity is pinned\")end local incoming={schema=1,active=true,id=record.id,term=math.max(0,math.floor(tonumber(record.term)or 0)),revision=math.max(0,math.floor(tonumber(record.revision)or 0)),seen=now(),source=\"cloud\",}if type(current)==\"table\"and current.active==true then local currentTerm=math.max(0,math.floor(tonumber(current.term)or 0))local currentRevision=math.max(0,math.floor(tonumber(current.revision)or 0))if incoming.term<currentTerm or(incoming.term==currentTerm and incoming.revision<currentRevision)then current.seen=incoming.seen assert(store.write(AUTHORITY,current),\"could not refresh cloud auth authority\")return current,\"stale cloud authority response ignored\"end end assert(store.write(AUTHORITY,incoming),\"could not save cloud auth authority\")return incoming end function M.updateDirectory(directory,revision)if type(directory)~=\"table\"then return false,\"invalid account directory\"end local incomingRevision=math.max(0,math.floor(tonumber(revision)or 0))local current=store.readFresh(DIRECTORY,nil)local currentRevision=type(current)==\"table\"and math.max(0,math.floor(tonumber(current.revision)or 0))or-1 if currentRevision>incomingRevision then return true,\"stale directory update ignored\"end return store.write(DIRECTORY,{schema=1,revision=incomingRevision,users=directory})end function M.saveCentralSession(value)if type(value)~=\"table\"or not accountName(value.name)or type(value.role)~=\"string\"then return false,\"invalid central session\"end local session={sessionVersion=3,name=value.name,role=value.role,token=value.token,issued=value.issued,expires=value.expires,accountRevision=value.accountRevision,authority=value.authority}if type(session.token)~=\"string\"or#session.token<16 then return false,\"invalid central session token\"end store.write(CENTRAL_SESSION,session)M.current=nil return true end local function safeAudit(action,detail)pcall(audit.log,\"auth\",action,detail or{})end local function stamp(account)account.updated=now()account.origin=tostring(os.getComputerID())account.revision=math.max(0,math.floor(tonumber(account.revision)or 0))+1 return account end local function makeAccount(role,plainPassword,context)local record,err=password.record(plainPassword,password.newSalt(context),configuredWorkFactor)assert(record,err)record.role=role return stamp(record)end sessionPathDelete=function()if fs.exists(SESSION)then fs.delete(SESSION)end store.invalidate(SESSION)end local function saveSession()if M.current==nil then sessionPathDelete();return end local durable={sessionVersion=2,name=M.current.name,nonce=M.current.nonce,issued=M.current.issued,expires=M.current.expires,accountRevision=M.current.accountRevision,}store.write(SESSION,durable)end local function makeSession(name,account)local issued=now()M.current={name=name,role=account.role,nonce=password.newSalt(\"session:\"..name),issued=issued,expires=issued+M.SESSION_TTL_MS,accountRevision=tonumber(account.revision)or 0,}saveSession()return M.current end local function liveAccount(name)local all=users()return all[name],all end local function normaliseSession(saved,account)if type(saved)~=\"table\"or not account or account.disabled then return nil end if saved.sessionVersion==2 then local issued,expires,revision=tonumber(saved.issued),tonumber(saved.expires),tonumber(saved.accountRevision)if type(saved.nonce)~=\"string\"or#saved.nonce<16 or not issued or not expires or not revision then return nil end if expires<=now()or revision~=(tonumber(account.revision)or 0)then return nil end return{name=saved.name,role=account.role,nonce=saved.nonce,issued=issued,expires=expires,accountRevision=revision}end if saved.sessionVersion==nil and type(saved.role)==\"string\"then return\"legacy\"end return nil end function M.refreshSession()local central=authority()if central then store.invalidate(CENTRAL_SESSION)local saved=store.read(CENTRAL_SESSION,nil)local entry=type(saved)==\"table\"and centralDirectory().users[saved.name]or nil if type(saved)~=\"table\"or saved.sessionVersion~=3 or saved.authority~=central.id or type(saved.token)~=\"string\"or(tonumber(saved.expires)or 0)<=now()or type(entry)~=\"table\"or entry.disabled==true or saved.accountRevision~=entry.revision then M.current=nil if fs.exists(CENTRAL_SESSION)then fs.delete(CENTRAL_SESSION)end return nil end M.current={name=saved.name,role=entry.role,nonce=saved.token,issued=saved.issued,expires=saved.expires,accountRevision=saved.accountRevision,authority=central.id,central=true}return M.current end store.invalidate(SESSION)local saved=store.read(SESSION,nil)if type(saved)~=\"table\"or not accountName(saved.name)then M.current=nil;return nil end local account=liveAccount(saved.name)local session=normaliseSession(saved,account)if session==\"legacy\"then return makeSession(saved.name,account)end if not session then M.current=nil sessionPathDelete()return nil end M.current=session return M.current end local function throttleState()store.invalidate(THROTTLE)local data=store.read(THROTTLE,{})return type(data)==\"table\"and data or{}end local function saveThrottle(data)local entries={}for name,entry in pairs(data)do if accountName(name)and type(entry)==\"table\"and type(entry.last)==\"number\"then entries[#entries+1]={name=name,last=entry.last}end end table.sort(entries,function(a,b)return a.last>b.last end)local kept={}for index,entry in ipairs(entries)do if index<=M.LOGIN_THROTTLE.maxEntries then kept[entry.name]=data[entry.name]end end store.write(THROTTLE,kept)end local function locked(name)if not accountName(name)then return false end local entry=throttleState()[name]return entry and(tonumber(entry.lockedUntil)or 0)>now()or false end local function noteFailure(name,reason)if not accountName(name)then return end local data,entry,time=throttleState(),nil,now()entry=data[name]or{failures=0,first=time}if time-(tonumber(entry.first)or 0)>M.LOGIN_THROTTLE.windowMs then entry.failures,entry.first=0,time end entry.failures=math.max(0,math.floor(tonumber(entry.failures)or 0))+1 entry.last=time if entry.failures>=M.LOGIN_THROTTLE.failures then entry.lockedUntil=time+M.LOGIN_THROTTLE.lockMs end data[name]=entry saveThrottle(data)safeAudit(\"login.failure\",{account=name,reason=reason or\"invalid\",throttled=(entry.lockedUntil or 0)>time})end local function clearFailures(name)local data=throttleState()if data[name]~=nil then data[name]=nil;saveThrottle(data)end end function M.throttleStatus(name)local entry=accountName(name)and throttleState()[name]or nil if not entry then return{failures=0,lockedUntil=0}end return{failures=tonumber(entry.failures)or 0,lockedUntil=tonumber(entry.lockedUntil)or 0}end function M.setPasswordWorkFactor(value)local work,err=password.normaliseWorkFactor(value)assert(work,err)configuredWorkFactor=work return work end function M.passwordWorkFactor()return configuredWorkFactor end function M.bootstrap(initialPassword)if authority()then return false end local all=users()if next(all)then return false end local plainPassword=initialPassword if plainPassword==nil then term.write(\"Create CeetOS admin password: \");plainPassword=read(\"*\")end local account=makeAccount(\"admin\",plainPassword,\"admin\")all.admin=account assert(save(all),\"could not save administrator account\")makeSession(\"admin\",account)safeAudit(\"bootstrap\",{account=\"admin\"})return true end function M.login(name,plainPassword)if authority()then local client=require((\"ceetos.lib.auth_client\"))return client.login(name,plainPassword)end if not accountName(name)or type(plainPassword)~=\"string\"then noteFailure(name,\"invalid\")return false,\"invalid credentials\"end if locked(name)then safeAudit(\"login.failure\",{account=name,reason=\"throttled\",throttled=true})return false,\"too many failed attempts; try again later\"end local account,all=liveAccount(name)if not account or account.disabled then noteFailure(name,\"invalid\")return false,\"invalid credentials\"end local valid,state=password.verify(account,plainPassword)if not valid then noteFailure(name,\"invalid\")return false,\"invalid credentials\"end if state==\"legacy\"then local upgraded=makeAccount(account.role,plainPassword,\"upgrade:\"..name)upgraded.updated=account.updated or upgraded.updated upgraded.origin=account.origin or upgraded.origin upgraded.revision=math.max(tonumber(account.revision)or 0,upgraded.revision)stamp(upgraded)all[name]=upgraded assert(save(all),\"could not upgrade password record\")account=upgraded safeAudit(\"password.upgraded\",{account=name})end clearFailures(name)makeSession(name,account)safeAudit(\"login.success\",{account=name})return true end function M.startLogin(name,plainPassword)if not authority()then return{done=true,ok=false,error=\"an Auth Server login is required\"}end return require(\"ceetos.lib.auth_client\").startLogin(name,plainPassword)end function M.step(task,event,eventId)if type(task)~=\"table\"then return true,false,\"invalid authentication transaction\"end if task.done then return true,task.ok==true,task.error end if not authority()then return true,false,\"an Auth Server login is required\"end return require(\"ceetos.lib.auth_client\").step(task,event,eventId)end function M.logout()M.current=nil sessionPathDelete()if fs.exists(CENTRAL_SESSION)then fs.delete(CENTRAL_SESSION)end end function M.add(name,role,plainPassword)if authority()then assert(rank[role]and accountName(name),\"invalid user or role\")local record,err=makeAccount(role,plainPassword,\"central:\"..name)assert(record,err)return require((\"ceetos.lib.auth_client\")).mutate(\"add\",{name=name,role=role,record=record})end M.require(\"operator\")assert(rank[role],\"invalid role\")if role==\"admin\"then M.require(\"admin\")end assert(accountName(name),\"invalid user name\")local all=users()assert(not all[name],\"user exists\")all[name]=makeAccount(role,plainPassword,name)assert(save(all),\"could not save user\")safeAudit(\"user.add\",{account=name,role=role})end local function adminCount(all)local count=0 for _,account in pairs(all)do if account.role==\"admin\"and not account.disabled then count=count+1 end end return count end function M.setRole(name,role)if authority()then return require((\"ceetos.lib.auth_client\")).mutate(\"role\",{name=name,role=role})end M.require(\"operator\")assert(accountName(name)and rank[role],\"invalid user or role\")local all,account=users(),users()[name]assert(account,\"unknown user\")if role==\"admin\"or account.role==\"admin\"then M.require(\"admin\")end assert(not(M.current and M.current.name==name),\"cannot change the active account role\")assert(not(account.role==\"admin\"and role~=\"admin\"and adminCount(all)<=1),\"cannot remove the last administrator\")account.role=role stamp(account)all[name]=account assert(save(all),\"could not save account role\")safeAudit(\"user.role\",{account=name,role=role})end function M.setDisabled(name,disabled)if authority()then return require((\"ceetos.lib.auth_client\")).mutate(\"disable\",{name=name,disabled=disabled==true})end M.require(\"admin\")assert(accountName(name),\"invalid user name\")local all,account=users(),users()[name]assert(account,\"unknown user\")assert(not(M.current and M.current.name==name),\"cannot disable the active account\")if disabled then assert(not(account.role==\"admin\"and adminCount(all)<=1),\"cannot disable the last administrator\")end account.disabled=disabled==true stamp(account)all[name]=account assert(save(all),\"could not save account state\")safeAudit(\"user.disabled\",{account=name,disabled=account.disabled})end function M.remove(name)if authority()then return require((\"ceetos.lib.auth_client\")).mutate(\"remove\",{name=name})end M.require(\"operator\")assert(accountName(name),\"invalid user name\")local all,account=users(),users()[name]assert(account,\"unknown user\")if account.role==\"admin\"then M.require(\"admin\")end assert(not(M.current and M.current.name==name),\"cannot delete the active account\")assert(not(account.role==\"admin\"and adminCount(all)<=1),\"cannot delete the last administrator\")all[name]=nil local removed=tombstones()removed[name]=now()assert(saveTombstones(removed),\"could not save account tombstone\")assert(save(all),\"could not remove account\")safeAudit(\"user.remove\",{account=name})end local function recoveryActive()store.invalidate(RECOVERY)local recovery=store.read(RECOVERY,{})return type(recovery)==\"table\"and(tonumber(recovery.until_ts)or 0)>now()end local function setPassword(name,plainPassword)assert(accountName(name),\"invalid user name\")local all,account=users(),users()[name]assert(account,\"unknown user\")local replacement=makeAccount(account.role,plainPassword,\"reset:\"..name)replacement.updated=account.updated or replacement.updated replacement.origin=account.origin or replacement.origin replacement.revision=math.max(tonumber(account.revision)or 0,replacement.revision)replacement.disabled=account.disabled==true stamp(replacement)all[name]=replacement assert(save(all),\"could not save password\")if M.current and M.current.name==name then M.current=nil;sessionPathDelete()end safeAudit(\"password.reset\",{account=name})return true end function M.resetPassword(name,plainPassword)if authority()then local account=centralDirectory().users[name];assert(account,\"unknown user\")local record,err=makeAccount(account.role,plainPassword,\"central-reset:\"..name);assert(record,err)return require((\"ceetos.lib.auth_client\")).mutate(\"reset\",{name=name,record=record})end if not M.allowed(\"admin\")and not recoveryActive()then error(\"permission denied (requires admin or active recovery window)\",2)end return setPassword(name,plainPassword)end function M.adminResetPassword(name,plainPassword)if authority()then local account=centralDirectory().users[name];assert(account,\"unknown user\")local record,err=makeAccount(account.role,plainPassword,\"central-reset:\"..name);assert(record,err)return require((\"ceetos.lib.auth_client\")).mutate(\"reset\",{name=name,record=record})end M.require(\"operator\")local account=users()[name]assert(account,\"unknown user\")if account.role==\"admin\"then M.require(\"admin\")end return setPassword(name,plainPassword)end function M.allowed(role)if not rank[role]then return false end local current=M.refreshSession()return current~=nil and rank[current.role]>=rank[role]end function M.sessionStatus()local current=M.refreshSession()if not current then return{active=false,central=authority()~=nil}end return{active=true,name=current.name,role=current.role,central=current.central==true,authority=current.authority,expires=tonumber(current.expires),accountRevision=tonumber(current.accountRevision),}end function M.canExitOs()return M.allowed(\"admin\")end function M.require(role)if not M.allowed(role)then error(\"permission denied (requires \"..tostring(role)..\")\",2)end end function M.list()local out={}local source=authority()and centralDirectory().users or users()for name,account in pairs(source)do out[#out+1]={name=name,role=account.role,disabled=account.disabled==true,revision=account.revision}end table.sort(out,function(a,b)return a.name<b.name end)return out end function M.conflicts()local out={}for name,entry in pairs(conflicts())do if accountName(name)and type(entry)==\"table\"then out[#out+1]={name=name,detected=tonumber(entry.detected)or 0,occurrences=tonumber(entry.occurrences)or 1,localOrigin=entry.localOrigin,incomingOrigin=entry.incomingOrigin,localRole=entry.localRole,incomingRole=entry.incomingRole,localDisabled=entry.localDisabled==true,incomingDisabled=entry.incomingDisabled==true,localRevision=tonumber(entry.localRevision)or 0,incomingRevision=tonumber(entry.incomingRevision)or 0,}end end table.sort(out,function(a,b)return a.name<b.name end)return out end function M.dismissConflict(name)M.require(\"admin\")assert(accountName(name),\"invalid user name\")local entries=conflicts()if entries[name]==nil then return false,\"no account conflict for user\"end entries[name]=nil assert(saveConflicts(entries),\"could not resolve account conflict\")safeAudit(\"accounts.conflict_dismissed\",{account=name})return true end function M.snapshot()if authority()then return{users={},tombstones={},source=tostring(os.getComputerID()),schema=3,central=true}end return M.authoritySnapshot()end function M.authoritySnapshot()local copy={users={},tombstones={},source=tostring(os.getComputerID()),schema=2}for name,account in pairs(users())do copy.users[name]={role=account.role,salt=account.salt,verifier=account.verifier,passwordVersion=account.passwordVersion,scheme=account.scheme,workFactor=account.workFactor,revision=tonumber(account.revision)or 0,disabled=account.disabled==true,updated=account.updated or 0,origin=account.origin or copy.source,}end for name,removed in pairs(tombstones())do copy.tombstones[name]=removed end return copy end local function validIncomingPassword(account)if password.isModern(account)then local work=password.normaliseWorkFactor(account.workFactor)return work~=nil and type(account.verifier)==\"string\"and#account.verifier==64 and account.verifier:match(\"^[0-9a-f]+$\")~=nil end return type(account.salt)==\"string\"and#account.salt>0 and#account.salt<=256 and type(account.verifier)==\"string\"and account.verifier:match(\"^[0-9a-fA-F]+$\")~=nil and#account.verifier==8 end function M.merge(snapshot,authoritySeed)if authority()and authoritySeed~=true then return false,\"distributed account sync is disabled by central authority\"end if type(snapshot)~=\"table\"or type(snapshot.users)~=\"table\"then return false,\"invalid account sync\"end local all,removed,changed,conflicted=users(),tombstones(),false,false for name,deletedAt in pairs(snapshot.tombstones or{})do if accountName(name)and type(deletedAt)==\"number\"and deletedAt>(removed[name]or 0)then local localAccount=all[name]if not localAccount or(localAccount.updated or 0)<=deletedAt then all[name],removed[name],changed=nil,deletedAt,true end end end for name,account in pairs(snapshot.users)do if accountName(name)and type(account)==\"table\"and rank[account.role]and validIncomingPassword(account)then local updated=tonumber(account.updated)or 0 local localAccount,incomingOrigin=all[name],tostring(account.origin or snapshot.source or\"\")local localOrigin=localAccount and tostring(localAccount.origin or os.getComputerID())or\"\"if localAccount and incomingOrigin~=\"\"and localOrigin~=\"\"and incomingOrigin~=localOrigin and not sameSecurityRecord(localAccount,account)then local saved,conflictErr=recordConflict(name,localAccount,account,localOrigin,incomingOrigin)if saved then conflicted=true safeAudit(\"accounts.conflict_quarantined\",{account=name,localOrigin=localOrigin,incomingOrigin=incomingOrigin})else return false,\"could not persist account conflict: \"..tostring(conflictErr)end else local wins=not localAccount or updated>(localAccount.updated or 0)or(updated==(localAccount.updated or 0)and incomingOrigin~=\"\"and incomingOrigin<localOrigin)if(removed[name]==nil or updated>removed[name])and wins then all[name]={role=account.role,salt=account.salt,verifier=account.verifier,passwordVersion=account.passwordVersion,scheme=account.scheme,workFactor=account.workFactor,revision=math.max(0,math.floor(tonumber(account.revision)or 0)),disabled=account.disabled==true,updated=updated,origin=incomingOrigin,}changed=true end end end end if changed then assert(save(all),\"could not save account sync\")assert(saveTombstones(removed),\"could not save account tombstones\")M.refreshSession()end if conflicted then return changed,\"account conflict quarantined for administrator review\"end return changed end function M.authorityAccount(name)if not accountName(name)then return nil end return users()[name]end function M.authorityDirectory()local out={}for name,account in pairs(users())do out[name]={role=account.role,disabled=account.disabled==true,revision=tonumber(account.revision)or 0}end return out end function M.authorityMutate(action,fields)fields=type(fields)==\"table\"and fields or{}local name=fields.name assert(accountName(name),\"invalid user name\")local all,account=users(),users()[name]if action==\"add\"then assert(not account,\"user exists\")assert(rank[fields.role],\"invalid role\")local record=fields.record assert(type(record)==\"table\"and password.isModern(record),\"central account mutation requires a verifier record\")record.role,record.disabled=fields.role,false;all[name]=stamp(record);assert(save(all),\"could not save user\")elseif action==\"role\"then assert(account and rank[fields.role],\"unknown user or invalid role\")if account.role==\"admin\"and fields.role~=\"admin\"then local admins=0;for _,candidate in pairs(all)do if candidate.role==\"admin\"and not candidate.disabled then admins=admins+1 end end assert(admins>1,\"cannot demote the last active admin\")end account.role=fields.role;stamp(account);all[name]=account;assert(save(all),\"could not save role\")elseif action==\"disable\"then if account and account.role==\"admin\"and fields.disabled==true then local admins=0;for _,candidate in pairs(all)do if candidate.role==\"admin\"and not candidate.disabled then admins=admins+1 end end assert(admins>1,\"cannot disable the last active admin\")end assert(account,\"unknown user\");account.disabled=fields.disabled==true;stamp(account);all[name]=account;assert(save(all),\"could not save user state\")elseif action==\"remove\"then assert(account,\"unknown user\")if account.role==\"admin\"and not account.disabled then local admins=0;for _,candidate in pairs(all)do if candidate.role==\"admin\"and not candidate.disabled then admins=admins+1 end end assert(admins>1,\"cannot remove the last active admin\")end all[name]=nil;local removed=tombstones();removed[name]=now();assert(saveTombstones(removed),\"could not save tombstone\");assert(save(all),\"could not remove user\")elseif action==\"reset\"then assert(account,\"unknown user\")local replacement=fields.record assert(type(replacement)==\"table\"and password.isModern(replacement),\"central password reset requires a verifier record\")replacement.role,replacement.disabled=account.role,account.disabled==true;all[name]=stamp(replacement);assert(save(all),\"could not save password\")else error(\"unsupported account mutation\")end safeAudit(\"authority.\"..action,{account=name})return M.authorityDirectory()end M.refreshSession()return M",
  ["ceetos/lib/auth_transport_policy.lua"] = "local M={}function M.isLogin(typeName)return typeName==\"auth_login_begin\"or typeName==\"auth_login_proof\"end function M.canSend(typeName,directTrusted)return M.isLogin(typeName)or directTrusted==true end function M.denial(typeName)if M.isLogin(typeName)then return nil end return\"central Auth Server account changes require a direct trusted link during security migration\"end return M",
  ["ceetos/lib/account_sync.lua"] = "local M={}function M.broadcast(auth,net,observe)if not auth or not net then return{sent=0,failed=0,total=0}end if auth.centralActive and auth.centralActive()then return{sent=0,failed=0,total=0,central=true}end local peers=type(net.directPeers)==\"function\"and net.directPeers()or{}local snapshot=type(auth.snapshot)==\"function\"and auth.snapshot()or nil if type(snapshot)~=\"table\"then return{sent=0,failed=#peers,total=#peers,error=\"account snapshot is unavailable\"}end local sent,failed,total=0,0,#peers if observe then observe({phase=\"starting\",completed=0,total=total})end for index,peer in ipairs(peers)do local ok=false if type(peer)==\"table\"and peer.id~=nil and type(net.request)==\"function\"then ok=net.request(peer.id,\"account_sync\",snapshot,{confidential=true})==true end if ok then sent=sent+1 else failed=failed+1 end if observe then observe({phase=\"sending\",completed=index,total=total,sent=sent,failed=failed})end end if observe then observe({phase=\"complete\",completed=total,total=total,sent=sent,failed=failed})end return{sent=sent,failed=failed,total=total}end function M.receive(auth,packet)if type(packet)~=\"table\"or packet.confidential~=true then return false,\"confidential account sync is required\"end if not auth or type(auth.merge)~=\"function\"then return false,\"account service is unavailable\"end return auth.merge(packet.body)end return M",
  ["ceetos/lib/password.lua"] = "local M={}M.SCHEME=\"pbkdf2-hmac-sha256\"M.PASSWORD_VERSION=2 M.DEFAULT_WORK_FACTOR=256 M.MIN_WORK_FACTOR=64 M.MAX_WORK_FACTOR=2048 M.MIN_LENGTH=8 M.MAX_LENGTH=128 local MOD=4294967296 local band,bor,bxor,bnot=bit32.band,bit32.bor,bit32.bxor,bit32.bnot local rshift,lshift=bit32.rshift,bit32.lshift local unpack=table.unpack or unpack local K={0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2,}local initial={0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19,}local function add(...)local value=0 for i=1,select(\"#\",...)do value=(value+(select(i,...)or 0))%MOD end return value end local function rrotate(value,amount)if bit32.rrotate then return bit32.rrotate(value,amount)end return bor(rshift(value,amount),lshift(value,32-amount))end local function word(bytes,index)return bytes:byte(index)*16777216+bytes:byte(index+1)*65536+bytes:byte(index+2)*256+bytes:byte(index+3)end local function packWord(value)return string.char(band(rshift(value,24),0xff),band(rshift(value,16),0xff),band(rshift(value,8),0xff),band(value,0xff))end local function sha256Raw(value)local bitLength=#value*8 value=value..string.char(0x80)value=value..string.rep(\"\\0\",(56-(#value%64))%64)value=value..packWord(math.floor(bitLength/MOD))..packWord(bitLength%MOD)local h={unpack(initial)}for offset=1,#value,64 do if#value>16384 and offset>1 and((offset-1)/64)%128==0 and type(sleep)==\"function\"then sleep(0)end local w={}for i=1,16 do w[i]=word(value,offset+(i-1)*4)end for i=17,64 do local x,y=w[i-15],w[i-2]local s0=bxor(rrotate(x,7),rrotate(x,18),rshift(x,3))local s1=bxor(rrotate(y,17),rrotate(y,19),rshift(y,10))w[i]=add(w[i-16],s0,w[i-7],s1)end local a,b,c,d,e,f,g,hh=unpack(h)for i=1,64 do local s1=bxor(rrotate(e,6),rrotate(e,11),rrotate(e,25))local choice=bxor(band(e,f),band(bnot(e),g))local t1=add(hh,s1,choice,K[i],w[i])local s0=bxor(rrotate(a,2),rrotate(a,13),rrotate(a,22))local majority=bxor(band(a,b),band(a,c),band(b,c))local t2=add(s0,majority)hh,g,f,e,d,c,b,a=g,f,e,add(d,t1),c,b,a,add(t1,t2)end h[1],h[2],h[3],h[4]=add(h[1],a),add(h[2],b),add(h[3],c),add(h[4],d)h[5],h[6],h[7],h[8]=add(h[5],e),add(h[6],f),add(h[7],g),add(h[8],hh)end local result={}for i=1,8 do result[i]=packWord(h[i])end return table.concat(result)end local function hmac(key,message)if#key>64 then key=sha256Raw(key)end key=key..string.rep(\"\\0\",64-#key)local inner,outer={},{}for i=1,64 do local byte=key:byte(i)inner[i],outer[i]=string.char(bxor(byte,0x36)),string.char(bxor(byte,0x5c))end return sha256Raw(table.concat(outer)..sha256Raw(table.concat(inner)..message))end local function xorBytes(left,right)local out={}for i=1,#left do out[i]=string.char(bxor(left:byte(i),right:byte(i)))end return table.concat(out)end local function cooperate(round)if round%32~=0 or not(os and os.queueEvent and os.pullEvent)then return end os.queueEvent(\"ceetos_password_yield\")os.pullEvent(\"ceetos_password_yield\")end local function toHex(value)return(value:gsub(\".\",function(byte)return string.format(\"%02x\",byte:byte())end))end local function constantEqual(left,right)if type(left)~=\"string\"or type(right)~=\"string\"then return false end local differing=bxor(#left,#right)local length=math.max(#left,#right)for i=1,length do differing=bor(differing,bxor(left:byte(i)or 0,right:byte(i)or 0))end return differing==0 end function M.hmacSha256(key,message)if type(key)~=\"string\"or type(message)~=\"string\"then return nil,\"HMAC key and message must be strings\"end return toHex(hmac(key,message))end function M.sha256(value)if type(value)~=\"string\"then return nil,\"SHA-256 input must be a string\"end return toHex(sha256Raw(value))end function M.constantTimeEqual(left,right)return constantEqual(left,right)end function M.legacyDigest(value)local h=2166136261 for i=1,#value do h=bxor(h,value:byte(i))h=(h*16777619)%MOD end return string.format(\"%08x\",h)end function M.normaliseWorkFactor(value)value=tonumber(value)if not value or value%1~=0 then return nil,\"invalid password work factor\"end if value<M.MIN_WORK_FACTOR or value>M.MAX_WORK_FACTOR then return nil,\"password work factor must be \"..M.MIN_WORK_FACTOR..\"-\"..M.MAX_WORK_FACTOR end return value end function M.policy(password)if type(password)~=\"string\"then return false,\"password is required\"end if#password<M.MIN_LENGTH or#password>M.MAX_LENGTH then return false,\"password must contain \"..M.MIN_LENGTH..\"-\"..M.MAX_LENGTH..\" characters\"end if password:find(\"[%z\\1-\\31\\127]\")then return false,\"password contains control characters\"end return true end function M.derive(password,salt,workFactor)local work,err=M.normaliseWorkFactor(workFactor)if not work then return nil,err end if type(password)~=\"string\"or type(salt)~=\"string\"or#salt<16 or#salt>256 then return nil,\"invalid password record\"end local block=string.char(0,0,0,1)local value=hmac(password,salt..block)local result=value for _=2,work do value=hmac(password,value)result=xorBytes(result,value)cooperate(_)end return toHex(result)end local saltCounter=0 function M.newSalt(context)saltCounter=saltCounter+1 local now=os.epoch and os.epoch(\"utc\")or 0 local computer=os.getComputerID and os.getComputerID()or 0 local label=os.getComputerLabel and os.getComputerLabel()or\"\"local random=math.random and math.random(0,2147483647)or 0 return toHex(sha256Raw(table.concat({tostring(now),tostring(computer),tostring(label),tostring(context or\"\"),tostring(saltCounter),tostring(random)},\":\"))):sub(1,32)end function M.record(password,salt,workFactor)local valid,err=M.policy(password)if not valid then return nil,err end local work,workErr=M.normaliseWorkFactor(workFactor or M.DEFAULT_WORK_FACTOR)if not work then return nil,workErr end salt=salt or M.newSalt()local verifier,deriveErr=M.derive(password,salt,work)if not verifier then return nil,deriveErr end return{passwordVersion=M.PASSWORD_VERSION,scheme=M.SCHEME,salt=salt,verifier=verifier,workFactor=work,}end function M.isModern(record)return type(record)==\"table\"and record.passwordVersion==M.PASSWORD_VERSION and record.scheme==M.SCHEME end function M.verify(record,password)if type(record)~=\"table\"or type(password)~=\"string\"or type(record.salt)~=\"string\"or type(record.verifier)~=\"string\"then return false,\"invalid password record\"end if M.isModern(record)then local verifier,err=M.derive(password,record.salt,record.workFactor)if not verifier then return false,err end return constantEqual(record.verifier,verifier),nil end return constantEqual(record.verifier,M.legacyDigest(record.salt..password)),\"legacy\"end return M",
  ["ceetos/lib/release_keys.lua"] = "return{algorithm=\"lamport-sha256-v1\",roots={[\"desktop-release-0.17.0\"]={public=\"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\",profile=\"desktop\"},[\"desktop-release-0.17.1\"]={public=\"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\",profile=\"desktop\"},},}",
  ["ceetos/lib/release_verify.lua"] = "local password=require(\"ceetos.lib.password\")local roots=require(\"ceetos.lib.release_keys\")local profile=require(\"ceetos.lib.profile\")local M={}M.CACHE_ROOT=\"/ceetos-updates\"M.CACHE_FILE=\"/ceetos-updates/release.lua\"M.META_FILE=\"/ceetos-updates/cache.lua\"M.PART_FILE=\"/ceetos-updates/release.part\"M.PENDING_FILE=\"/ceetos-updates/pending.lua\"M.MAX_PACKAGE_BYTES=1024*1024 M.MAX_MANIFEST_BYTES=48*1024 M.CHUNK_BYTES=3072 M.KEY_PARTS=256 local alphabet=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\"local decodeMap={}for i=1,#alphabet do decodeMap[alphabet:sub(i,i)]=i-1 end local function read(path)if not fs.exists(path)or fs.isDir(path)then return nil end local handle=fs.open(path,\"r\")if not handle then return nil end local value=handle.readAll();handle.close()return value end local function write(path,value)local parent=fs.getDir(path)if parent~=\"\"and not fs.exists(parent)then fs.makeDir(parent)end local temporary=path..\".tmp\"if fs.exists(temporary)then fs.delete(temporary)end local handle=assert(fs.open(temporary,\"w\"),\"could not create temporary release state\")handle.write(value);handle.close()if fs.exists(path)then fs.delete(path)end fs.move(temporary,path)end local function encode(value)local out,index={},1 while index<=#value do local a,b,c=value:byte(index,index+2)local n=(a or 0)*65536+(b or 0)*256+(c or 0)out[#out+1]=alphabet:sub(math.floor(n/262144)%64+1,math.floor(n/262144)%64+1)out[#out+1]=alphabet:sub(math.floor(n/4096)%64+1,math.floor(n/4096)%64+1)out[#out+1]=b and alphabet:sub(math.floor(n/64)%64+1,math.floor(n/64)%64+1)or\"=\"out[#out+1]=c and alphabet:sub(n%64+1,n%64+1)or\"=\"index=index+3 end return table.concat(out)end local function decode(value)if type(value)~=\"string\"or#value%4~=0 or#value>M.MAX_MANIFEST_BYTES*2 then return nil,\"invalid base64\"end local out={}for index=1,#value,4 do local a,b,c,d=value:sub(index,index),value:sub(index+1,index+1),value:sub(index+2,index+2),value:sub(index+3,index+3)if not decodeMap[a]or not decodeMap[b]or(c~=\"=\"and not decodeMap[c])or(d~=\"=\"and not decodeMap[d])then return nil,\"invalid base64\"end if(c==\"=\"and d~=\"=\")or((c==\"=\"or d==\"=\")and index+3~=#value)then return nil,\"invalid base64 padding\"end local n=decodeMap[a]*262144+decodeMap[b]*4096+(decodeMap[c]or 0)*64+(decodeMap[d]or 0)out[#out+1]=string.char(math.floor(n/65536)%256)if c~=\"=\"then out[#out+1]=string.char(math.floor(n/256)%256)end if d~=\"=\"then out[#out+1]=string.char(n%256)end end return table.concat(out)end local function unhex(value)if type(value)~=\"string\"or#value%2~=0 or value:find(\"[^%x]\")then return nil end return(value:gsub(\"..\",function(pair)return string.char(tonumber(pair,16))end))end local function semver(value)if type(value)~=\"string\"then return nil end local major,minor,patch=value:match(\"^(%d+)%.(%d+)%.(%d+)$\")if not major then return nil end return tonumber(major),tonumber(minor),tonumber(patch)end function M.compareVersions(left,right)local a,b,c=semver(left);local x,y,z=semver(right)if not a or not x then return nil,\"invalid semantic version\"end if a~=x then return a<x and-1 or 1 end if b~=y then return b<y and-1 or 1 end if c~=z then return c<z and-1 or 1 end return 0 end local function canonical(manifest)if manifest._legacyProfile==true then return table.concat({\"ceetos-release-v1\",tostring(manifest.channel),tostring(manifest.version),tostring(manifest.size),tostring(manifest.digest),tostring(manifest.key_id),tostring(manifest.next_key_id),tostring(manifest.next_public),},\"|\")end return table.concat({\"ceetos-release-v2\",tostring(manifest.channel),tostring(manifest.profile),tostring(manifest.version),tostring(manifest.size),tostring(manifest.digest),tostring(manifest.key_id),tostring(manifest.next_key_id),tostring(manifest.next_public),},\"|\")end local function unpackKey(value)local raw,err=decode(value)if not raw or#raw~=M.KEY_PARTS*2*32 then return nil,err or\"invalid public key\"end local parts={}for index=1,M.KEY_PARTS*2 do parts[index]=raw:sub((index-1)*32+1,index*32)end return parts end function M.validPublicKey(value)return unpackKey(value)end local function lookupKey(keyId)local saved=M.trustedKeys()local value=saved[keyId]if value then return unpackKey(value)end value=roots.roots and roots.roots[keyId]if type(value)==\"table\"and type(value.public)==\"string\"then value=value.public end return unpackKey(value)end local function bitAt(digest,index)local byte=digest:byte(math.floor((index-1)/8)+1)return bit32.band(bit32.rshift(byte,7-((index-1)%8)),1)end local function decodeSignature(value)local raw,err=decode(value)if not raw or#raw~=M.KEY_PARTS*32 then return nil,err or\"invalid signature\"end local parts={}for index=1,M.KEY_PARTS do parts[index]=raw:sub((index-1)*32+1,index*32)end return parts end local function verifySignature(manifest,signature)if roots.algorithm~=\"lamport-sha256-v1\"then return false,\"unsupported release signing algorithm\"end local public,publicErr=lookupKey(manifest.key_id)local parts,signatureErr=decodeSignature(signature)if not public or not parts then local prefix=not public and\"invalid release public key: \"or\"invalid release signature: \"return false,prefix..tostring(publicErr or signatureErr or\"unknown release key\")end local digest=unhex(password.sha256(canonical(manifest))or\"\")if not digest then return false,\"could not hash release manifest\"end for index=1,M.KEY_PARTS do local expected=public[(index-1)*2+bitAt(digest,index)+1]local candidate=unhex(password.sha256(parts[index])or\"\")if not candidate or not password.constantTimeEqual(candidate,expected)then return false,\"invalid release signature\"end end return true end local function validateManifest(manifest)if type(manifest)~=\"table\"or manifest.channel~=\"release\"then return nil,\"only signed release packages are accepted\"end if manifest.profile==nil then manifest.profile,manifest._legacyProfile=\"desktop\",true end if type(manifest.profile)~=\"string\"or not profile.IDS[manifest.profile]then return nil,\"invalid release profile\"end if not semver(manifest.version)or type(manifest.size)~=\"number\"or manifest.size<1 or manifest.size>M.MAX_PACKAGE_BYTES then return nil,\"invalid release manifest size or version\"end if type(manifest.digest)~=\"string\"or#manifest.digest~=64 or manifest.digest:find(\"[^%x]\")then return nil,\"invalid release digest\"end if type(manifest.key_id)~=\"string\"or#manifest.key_id<1 or#manifest.key_id>64 then return nil,\"invalid release key id\"end if type(manifest.next_key_id)~=\"string\"or#manifest.next_key_id<1 or#manifest.next_key_id>64 then return nil,\"invalid next release key id\"end local nextKey=unpackKey(manifest.next_public)if not nextKey then return nil,\"invalid successor release key\"end return manifest end function M.parse(source)if type(source)~=\"string\"or#source>M.MAX_PACKAGE_BYTES then return nil,\"release package is too large\"end local encodedManifest,encodedSignature=source:match(\"^%-%- CEETOS_RELEASE_MANIFEST:([^\\r\\n]+)[\\r\\n]+%-%- CEETOS_RELEASE_SIGNATURE:([^\\r\\n]+)\")if not encodedManifest then encodedManifest,encodedSignature=source:match(\"^%-%- CeetOS signed release%. Generated; do not edit%.[\\r\\n]+%-%- CEETOS_RELEASE_MANIFEST:([^\\r\\n]+)[\\r\\n]+%-%- CEETOS_RELEASE_SIGNATURE:([^\\r\\n]+)\")end local first=source:find(\"%-%- CEETOS_RELEASE_PAYLOAD_BEGIN[\\r\\n]+\")if not encodedManifest or not encodedSignature or not first then return nil,\"not a signed CeetOS release package\"end local decoded,decodeErr=decode(encodedManifest)if not decoded then return nil,\"invalid release manifest encoding: \"..tostring(decodeErr)end local loader=load(\"return \"..decoded,\"=release-manifest\",\"t\",{})if not loader then return nil,\"invalid release manifest\"end local ok,manifest=pcall(loader)if not ok then return nil,\"invalid release manifest\"end local valid,manifestErr=validateManifest(manifest)if not valid then return nil,manifestErr end local start=source:find(\"\\n\",first)+1 local finish=start+valid.size-1 local payload=source:sub(start,finish)local suffix=source:sub(finish+1)if suffix~=\"\\n-- CEETOS_RELEASE_PAYLOAD_END\\n\"and suffix~=\"\\n-- CEETOS_RELEASE_PAYLOAD_END\"and suffix~=\"\\r\\n-- CEETOS_RELEASE_PAYLOAD_END\\r\\n\"and suffix~=\"\\r\\n-- CEETOS_RELEASE_PAYLOAD_END\"then return nil,\"release payload boundary mismatch\"end if#payload~=valid.size or not password.constantTimeEqual(password.sha256(payload)or\"\",valid.digest)then return nil,\"release payload digest mismatch\"end local signed,signatureErr=verifySignature(valid,encodedSignature)if not signed then return nil,signatureErr end return{manifest=valid,signature=encodedSignature,payload=payload,source=source}end function M.trustedKeys()local raw=read(M.CACHE_ROOT..\"/trusted-keys.lua\")if not raw then return{}end local loader=load(raw,\"=trusted-release-keys\",\"t\",{})if not loader then return{}end local ok,value=pcall(loader)return ok and type(value)==\"table\"and value or{}end local function saveKeys(value)local encoded=textutils.serialise(value,{compact=true})write(M.CACHE_ROOT..\"/trusted-keys.lua\",\"return \"..encoded)end function M.metadata()local raw=read(M.META_FILE)if not raw then return nil end local loader=load(raw,\"=release-cache\",\"t\",{})if not loader then return nil end local ok,value=pcall(loader)return ok and type(value)==\"table\"and value or nil end function M.cache(source,provider)local package,err=M.parse(source)if not package then return nil,err end if fs.exists(M.CACHE_ROOT)and fs.isDir(M.CACHE_ROOT)then if fs.exists(M.PART_FILE)then fs.delete(M.PART_FILE)end elseif not fs.exists(M.CACHE_ROOT)then fs.makeDir(M.CACHE_ROOT)end write(M.CACHE_FILE,package.source)local metadata={schema=2,channel=package.manifest.channel,profile=package.manifest.profile,version=package.manifest.version,size=package.manifest.size,digest=package.manifest.digest,keyId=package.manifest.key_id,nextKeyId=package.manifest.next_key_id,source=provider and tostring(provider)or\"local\",cachedAt=os.epoch(\"utc\"),}write(M.META_FILE,\"return \"..textutils.serialise(metadata,{compact=true}))local keys=M.trustedKeys();keys[package.manifest.next_key_id]=package.manifest.next_public;saveKeys(keys)return metadata end function M.promotePart(path,provider)if type(path)~=\"string\"or path~=M.PART_FILE then return nil,\"invalid release staging path\"end local source=read(path)local package,err=M.parse(source)if not package then return nil,err end if not fs.exists(M.CACHE_ROOT)then fs.makeDir(M.CACHE_ROOT)end if fs.exists(M.CACHE_FILE)then fs.delete(M.CACHE_FILE)end fs.move(M.PART_FILE,M.CACHE_FILE)local metadata={schema=2,channel=package.manifest.channel,profile=package.manifest.profile,version=package.manifest.version,size=package.manifest.size,digest=package.manifest.digest,keyId=package.manifest.key_id,nextKeyId=package.manifest.next_key_id,source=provider and tostring(provider)or\"local\",cachedAt=os.epoch(\"utc\"),}write(M.META_FILE,\"return \"..textutils.serialise(metadata,{compact=true}))M.adopt(package)return metadata end function M.adopt(package)if type(package)~=\"table\"or type(package.manifest)~=\"table\"then return false,\"invalid verified package\"end local keys=M.trustedKeys()keys[package.manifest.next_key_id]=package.manifest.next_public saveKeys(keys)return true end function M.cachedPackage()local source,metadata=read(M.CACHE_FILE),M.metadata()if not source then return nil end local package,err=M.parse(source)if not package then return nil,err end if not metadata then metadata={schema=2,channel=package.manifest.channel,profile=package.manifest.profile,version=package.manifest.version,size=package.manifest.size,digest=package.manifest.digest,keyId=package.manifest.key_id,nextKeyId=package.manifest.next_key_id,source=\"local installer\",cachedAt=os.epoch(\"utc\"),}write(M.META_FILE,\"return \"..textutils.serialise(metadata,{compact=true}))end M.adopt(package)return package,metadata end function M.clear()if fs.exists(M.PART_FILE)then fs.delete(M.PART_FILE)end if fs.exists(M.CACHE_FILE)then fs.delete(M.CACHE_FILE)end if fs.exists(M.META_FILE)then fs.delete(M.META_FILE)end return true end function M.encode(value)return encode(value)end function M.decode(value)return decode(value)end function M.canonical(manifest)return canonical(manifest)end return M",
  ["ceetos/lib/updater.lua"] = "local store=require(\"ceetos.lib.store\")local release=require(\"ceetos.lib.release_verify\")local version=require(\"ceetos.lib.version\")local net=require(\"ceetos.lib.net\")local profile=require(\"ceetos.lib.profile\")local M={}local STATE_PATH=\"/ceetos/data/update-status.lua\"local OFFER_LIMIT,HISTORY_LIMIT=20,16 local BEACON_INTERVAL,REQUEST_TIMEOUT,REQUEST_LIMIT=30000,3500,3 local MAX_OFFER_AGE=5*60*1000 local CLOUD_RELEASE_ORIGIN=\"https://releases.ceet.uk\"local CLOUD_MANIFEST_URL=CLOUD_RELEASE_ORIGIN..\"/manifest.json\"local CLOUD_SOURCE_ID=\"cloud.releases.ceet.uk\"local state=nil local lastBeacon,lastCacheRead=0,0 local cached=nil local publicChunkWindows={}local PUBLIC_CHUNK_WINDOW_MS,PUBLIC_CHUNK_LIMIT,PUBLIC_CHUNK_CLIENTS=3000,8,32 local publicChunkEligibility={}local PUBLIC_TRANSFER_ELIGIBILITY_MS,PUBLIC_TRANSFER_ELIGIBILITY_LIMIT=15000,32 local function now()return os.epoch(\"utc\")end local function localId()return tostring(os.getComputerID())end local function localLabel()return os.getComputerLabel()or(\"Computer \"..localId())end local function publicChunkAllowed(source,transfer)source,transfer=tostring(source or\"\"),tostring(transfer or\"\")if#source==0 or#source>64 or not transfer:match(\"^[%w_%-%.]+$\")or#transfer>128 then return false,\"invalid public update transfer\"end local timestamp,row=now(),publicChunkWindows[source]if not row or timestamp-row.started>=PUBLIC_CHUNK_WINDOW_MS then row={started=timestamp,count=0,transfers={}}publicChunkWindows[source]=row end row.transfers[transfer]=timestamp row.count=row.count+1 if row.count>PUBLIC_CHUNK_LIMIT then return false,\"public update chunk rate limit exceeded\"end local clients={}for client,state in pairs(publicChunkWindows)do clients[#clients+1]={id=client,at=tonumber(state.started)or 0}end table.sort(clients,function(a,b)return a.at>b.at end)for index=PUBLIC_CHUNK_CLIENTS+1,#clients do publicChunkWindows[clients[index].id]=nil end return true end local function grantPublicTransferEligibility(source,requestedProfile)local timestamp=now()source=tostring(source or\"\")if#source==0 or#source>64 or requestedProfile~=profile.id()then return end publicChunkEligibility[source]=timestamp+PUBLIC_TRANSFER_ELIGIBILITY_MS local rows={}for client,expires in pairs(publicChunkEligibility)do if tonumber(expires)and expires>timestamp then rows[#rows+1]={id=client,expires=expires}else publicChunkEligibility[client]=nil end end table.sort(rows,function(a,b)return a.expires>b.expires end)for index=PUBLIC_TRANSFER_ELIGIBILITY_LIMIT+1,#rows do publicChunkEligibility[rows[index].id]=nil end end local function defaults()return{schema=1,offers={},history={},revision=0,status=\"idle\",message=nil,transfer=nil,cache=nil}end local function loadState()if state then return state end state=store.read(STATE_PATH,defaults())for key,value in pairs(defaults())do if state[key]==nil then state[key]=value end end state.offers=type(state.offers)==\"table\"and state.offers or{}state.history=type(state.history)==\"table\"and state.history or{}return state end local function save()local value=loadState()value.revision=(tonumber(value.revision)or 0)+1 local ok,err=store.write(STATE_PATH,value)if ok and os and os.queueEvent then pcall(os.queueEvent,\"ceetos_state_changed\",\"updates\",value.revision)end return ok,err end local function record(kind,fields)local value=loadState()local row={at=now(),kind=tostring(kind):sub(1,48)}for key,item in pairs(fields or{})do if key~=\"data\"and key~=\"source\"and key~=\"signature\"then row[key]=item end end value.history[#value.history+1]=row while#value.history>HISTORY_LIMIT do table.remove(value.history,1)end end local function setStatus(status,message)local value=loadState()value.status,value.message=status,message and tostring(message):sub(1,180)or nil end local function validId(value)return type(value)==\"string\"and value:match(\"^[%w_%-%.]+$\")and#value<=96 end local function releaseFilename(profileId,releaseVersion)if profileId==\"desktop\"then return\"desktop/ceetos-release\"..releaseVersion..\".lua\"end return profileId..\"/ceetos-\"..profileId..\"-release\"..releaseVersion..\".lua\"end local function readHttp(url,limit)if not http or type(http.get)~=\"function\"then return nil,\"HTTP is unavailable; enable it to check releases.ceet.uk\"end local ok,response,requestErr=pcall(http.get,url,{[\"Accept\"]=\"application/json\"})if not ok or not response then return nil,tostring(requestErr or\"release server request failed\")end local chunks,total={},0 while true do local part=response.read(math.min(4096,limit-total+1))if not part or part==\"\"then break end total=total+#part if total>limit then pcall(response.close);return nil,\"release server response is too large\"end chunks[#chunks+1]=part end pcall(response.close)return table.concat(chunks)end local function validOffer(body)if type(body)~=\"table\"or type(body.version)~=\"string\"or type(body.size)~=\"number\"or type(body.digest)~=\"string\"or type(body.keyId)~=\"string\"or type(body.profile)~=\"string\"then return nil,\"malformed release offer\"end if body.profile~=profile.id()then return nil,\"release profile does not match this installation\"end local compare=release.compareVersions(body.version,version.string)if compare==nil then return nil,\"invalid release version\"end if body.size<1 or body.size>release.MAX_PACKAGE_BYTES then return nil,\"invalid release size\"end if#body.digest~=64 or body.digest:find(\"[^%x]\")then return nil,\"invalid release digest\"end if#body.keyId<1 or#body.keyId>64 then return nil,\"invalid release key ID\"end local filename=body.filename if filename~=nil and filename~=releaseFilename(body.profile,body.version)then return nil,\"invalid release filename\"end return{profile=body.profile,version=body.version,size=math.floor(body.size),digest=body.digest:lower(),keyId=body.keyId,filename=filename,fingerprint=body.digest:sub(1,16),newer=compare>0}end local function refreshCached()local timestamp=now()if cached and timestamp-lastCacheRead<5000 then return cached end lastCacheRead=timestamp local value=loadState()local previous=value.cache and(tostring(value.cache.version)..\":\"..tostring(value.cache.digest))or nil local package,metadata=release.cachedPackage()if package and package.manifest.profile==profile.id()then cached={package=package,metadata=metadata,offer={version=package.manifest.version,size=#package.source,digest=package.manifest.digest,profile=package.manifest.profile,keyId=package.manifest.key_id,fingerprint=package.manifest.digest:sub(1,16),}}value.cache={profile=package.manifest.profile,version=package.manifest.version,size=#package.source,digest=package.manifest.digest,keyId=package.manifest.key_id,fingerprint=package.manifest.digest:sub(1,16),source=metadata and metadata.source or\"local\",verifiedAt=timestamp}else cached=nil value.cache=nil end local current=value.cache and(tostring(value.cache.version)..\":\"..tostring(value.cache.digest))or nil if current~=previous then save()end return cached end local function providerOffer()local available=refreshCached()if not available then return nil end return{version=available.offer.version,size=available.offer.size,digest=available.offer.digest,profile=available.offer.profile,keyId=available.offer.keyId,label=localLabel(),id=localId(),}end local function upsertOffer(source,body,transport,distance)source=tostring(source)if source==localId()then return false,\"ignored own release offer\"end local offer,err=validOffer(body)if not offer then return false,err end if not offer.newer then return false,\"offered release is not newer\"end local value=loadState()local key=source..\":\"..offer.version..\":\"..offer.digest:sub(1,16)value.offers[key]={id=source,label=tostring(body.label or(\"Computer \"..source)):sub(1,96),version=offer.version,profile=offer.profile,size=offer.size,digest=offer.digest,fingerprint=offer.fingerprint,keyId=offer.keyId,filename=offer.filename,transport=transport,distance=tonumber(distance),seen=now(),key=key,}local rows={}for entryKey,item in pairs(value.offers)do rows[#rows+1]={key=entryKey,seen=tonumber(item.seen)or 0}end table.sort(rows,function(a,b)return a.seen>b.seen end)while#rows>OFFER_LIMIT do value.offers[table.remove(rows).key]=nil end setStatus(\"offers\",\"Signed release \"..offer.version..\" available from \"..tostring(body.label or source))record(\"offer\",{id=source,version=offer.version,transport=transport,distance=distance})save()return true end local function checkCloudManifest()local raw,readErr=readHttp(CLOUD_MANIFEST_URL,release.MAX_MANIFEST_BYTES)if not raw then return 0,readErr end local ok,catalogue=pcall(textutils.unserialiseJSON,raw)if not ok or type(catalogue)~=\"table\"or catalogue.schema~=1 or type(catalogue.releases)~=\"table\"or#catalogue.releases>OFFER_LIMIT then return 0,\"invalid signed-release catalogue\"end local count=0 for index=1,#catalogue.releases do local entry=catalogue.releases[index]if type(entry)==\"table\"and entry.profile==profile.id()then local accepted=upsertOffer(CLOUD_SOURCE_ID,entry,\"cloud\")if accepted then count=count+1 end end end return count end local function offers()local value,timestamp,rows=loadState(),now(),{}for key,item in pairs(value.offers)do if type(item)==\"table\"and timestamp-(tonumber(item.seen)or 0)<=MAX_OFFER_AGE then rows[#rows+1]=item else value.offers[key]=nil end end table.sort(rows,function(left,right)local compare=release.compareVersions(left.version,right.version)or 0 if compare~=0 then return compare>0 end return tostring(left.id)<tostring(right.id)end)return rows end local function findOffer(source)source=tostring(source)for _,item in ipairs(offers())do if item.key==source or tostring(item.id)==source then return item end end return nil,\"release source is unavailable; run update check first\"end local function removePart()if fs.exists(release.PART_FILE)then fs.delete(release.PART_FILE)end end local function appendPart(data)local handle,err=fs.open(release.PART_FILE,\"a\")if not handle then return false,err or\"could not open update staging file\"end handle.write(data);handle.close()return true end local function cachedSource()local available=refreshCached()return available and available.package and available.package.source end local function transferRequest(transfer)local length=math.min(release.CHUNK_BYTES,transfer.size-transfer.offset+1)if length<1 then return false,\"transfer is already complete\"end transfer.awaiting,transfer.requestedAt=true,now()local body={transfer=transfer.id,offset=transfer.offset,length=length}local sent,err if transfer.transport==\"routed\"then sent,err=net.request(transfer.source,\"update_chunk_request\",body,{safe=true})else sent,err=net.publicSend(transfer.source,\"update_chunk_request\",body)end if not sent then transfer.awaiting=false;return false,err end return true end local function beginDownload(source)local offer,err=findOffer(source)if not offer then return false,err end local compare=release.compareVersions(offer.version,version.string)if not compare or compare<=0 then return false,\"refusing a downgrade or current release\"end local free=fs.getFreeSpace and fs.getFreeSpace(\"/\")or nil if type(free)==\"number\"and free<offer.size+4096 then return false,\"not enough free space for one verified release cache\"end removePart()release.clear();cached=nil local value=loadState()if offer.transport==\"cloud\"then setStatus(\"downloading\",\"Downloading signed release \"..offer.version..\" from releases.ceet.uk\")save()local sourceText,readErr=readHttp(CLOUD_RELEASE_ORIGIN..\"/\"..tostring(offer.filename or\"\"),release.MAX_PACKAGE_BYTES)if not sourceText then setStatus(\"error\",readErr);record(\"download_failed\",{id=CLOUD_SOURCE_ID,error=readErr});save();return false,readErr end local parsed,parseErr=release.parse(sourceText)if not parsed or parsed.manifest.profile~=offer.profile or parsed.manifest.version~=offer.version or parsed.manifest.digest:lower()~=offer.digest or parsed.manifest.key_id~=offer.keyId then local reason=parseErr or\"verified package did not match selected release offer\"setStatus(\"error\",reason);record(\"download_failed\",{id=CLOUD_SOURCE_ID,error=reason});save();return false,reason end local metadata,cacheErr=release.cache(sourceText,\"releases.ceet.uk\")if not metadata then setStatus(\"error\",cacheErr);record(\"download_failed\",{id=CLOUD_SOURCE_ID,error=cacheErr});save();return false,cacheErr end cached=nil;refreshCached()setStatus(\"downloaded\",\"Verified release \"..metadata.version..\" is ready for local confirmation\")record(\"download_verified\",{id=CLOUD_SOURCE_ID,version=metadata.version,digest=metadata.digest:sub(1,16)})save()return true,metadata end value.transfer={id=\"update-\"..localId()..\"-\"..tostring(now())..\"-\"..tostring(math.random(1000,9999)),source=tostring(offer.id),label=offer.label,transport=offer.transport==\"routed\"and\"routed\"or\"direct\",profile=offer.profile,version=offer.version,size=offer.size,digest=offer.digest,keyId=offer.keyId,offset=1,received=0,retries=0,awaiting=false,started=now(),}setStatus(\"downloading\",\"Downloading signed release \"..offer.version..\" from \"..offer.label)record(\"download_started\",{id=offer.id,version=offer.version,transport=value.transfer.transport})local sent,sendErr=transferRequest(value.transfer)if not sent then value.transfer=nil;setStatus(\"error\",sendErr);record(\"download_failed\",{id=offer.id,error=sendErr})end save()return sent,sendErr end local function finishTransfer(transfer)local handle=fs.open(release.PART_FILE,\"r\")local source=handle and handle.readAll()or nil if handle then handle.close()end if not source or#source~=transfer.size then return false,\"download size did not match offer\"end local parsed,parseErr=release.parse(source)if not parsed then return false,parseErr end if parsed.manifest.profile~=profile.id()or parsed.manifest.profile~=transfer.profile or parsed.manifest.version~=transfer.version or parsed.manifest.digest:lower()~=transfer.digest:lower()or parsed.manifest.key_id~=transfer.keyId then return false,\"verified package did not match selected release offer\"end local metadata,promoteErr=release.promotePart(release.PART_FILE,transfer.label)if not metadata then return false,promoteErr end cached=nil;refreshCached()local value=loadState();value.transfer=nil setStatus(\"downloaded\",\"Verified release \"..metadata.version..\" is ready for local confirmation\")record(\"download_verified\",{id=transfer.source,version=metadata.version,digest=metadata.digest:sub(1,16)})save()return true,metadata end local function acceptChunk(source,body,transport)local value,transfer=loadState(),loadState().transfer if not transfer or transfer.transport~=transport or tostring(source)~=tostring(transfer.source)then return false,\"unexpected update chunk\"end if type(body)~=\"table\"or body.transfer~=transfer.id or math.floor(tonumber(body.offset)or 0)~=transfer.offset or type(body.data)~=\"string\"then return false,\"malformed update chunk\"end local remaining=transfer.size-transfer.offset+1 if#body.data<1 or#body.data>math.min(release.CHUNK_BYTES,remaining)then return false,\"invalid update chunk length\"end local ok,err=appendPart(body.data)if not ok then transfer.awaiting=false;setStatus(\"error\",err);record(\"download_failed\",{error=err});save();return false,err end transfer.offset,transfer.received,transfer.awaiting,transfer.retries=transfer.offset+#body.data,(transfer.received or 0)+#body.data,false,0 if transfer.offset>transfer.size then local complete,result=finishTransfer(transfer)if not complete then removePart();value.transfer=nil;setStatus(\"error\",result);record(\"download_failed\",{id=source,error=result});save()end return complete,result end local sent,sendErr=transferRequest(transfer)if not sent then setStatus(\"error\",sendErr);record(\"download_failed\",{id=source,error=sendErr});value.transfer=nil;removePart()end save()return sent,sendErr end local function serveChunk(target,body,routed,request)if type(body)~=\"table\"or not validId(body.transfer)or math.floor(tonumber(body.offset)or 0)<1 or math.floor(tonumber(body.length)or 0)<1 or tonumber(body.length)>release.CHUNK_BYTES then return false,\"invalid update chunk request\"end if not routed then if(tonumber(publicChunkEligibility[tostring(target)])or 0)<=now()then return false,\"public update transfer was not recently offered\"end local permitted,permitErr=publicChunkAllowed(target,body.transfer)if not permitted then return false,permitErr end end local source=cachedSource()if not source then return false,\"no verified newer signed release is cached\"end local offset,length=math.floor(body.offset),math.floor(body.length)if offset>#source then return false,\"requested update offset is beyond package\"end local data=source:sub(offset,math.min(#source,offset+length-1))local response={transfer=body.transfer,offset=offset,data=data}if routed then return net.reply(request,\"update_chunk\",response,{safe=true})end return net.publicSend(target,\"update_chunk\",response)end function M.publicPacket(packet,distance)local source,body=tostring(packet.from),packet.body or{}if packet.type==\"update_query\"then if body.profile~=profile.id()then return false,\"release profile does not match this installation\"end local offer=providerOffer()local compare=offer and release.compareVersions(offer.version,body.version or\"\")if offer and compare and compare>0 then grantPublicTransferEligibility(source,body.profile)net.publicSend(source,\"update_offer\",offer)end return elseif packet.type==\"update_offer\"then return upsertOffer(source,body,\"direct\",distance)elseif packet.type==\"update_chunk_request\"then return serveChunk(source,body,false)elseif packet.type==\"update_chunk\"then return acceptChunk(source,body,\"direct\")end end function M.authenticatedPacket(from,peer,packet)local body=packet.body or{}if packet.type==\"update_query\"then if body.profile~=profile.id()then return false,\"release profile does not match this installation\"end local offer=providerOffer()local compare=offer and release.compareVersions(offer.version,body.version or\"\")if offer and compare and compare>0 then return net.reply(packet,\"update_offer\",offer,{safe=true})end elseif packet.type==\"update_offer\"then return upsertOffer(from,body,\"routed\")elseif packet.type==\"update_chunk_request\"then return serveChunk(from,body,true,packet)elseif packet.type==\"update_chunk\"then return acceptChunk(from,body,\"routed\")end end function M.check()local value=loadState()for key in pairs(value.offers)do value.offers[key]=nil end setStatus(\"checking\",\"Looking for newer signed releases\")local cloudOffers,cloudErr=checkCloudManifest()local sent,err=net.publicBroadcast(\"update_query\",{version=version.string,profile=profile.id()})local routed=0 for _,peer in ipairs(net.peers())do if not peer.rekeyRequired then local ok=net.request(peer.id,\"update_query\",{version=version.string,profile=profile.id()},{safe=true})if ok then routed=routed+1 end end end if cloudErr then setStatus(\"warning\",\"Cloud release check failed: \"..tostring(cloudErr):sub(1,120))elseif cloudOffers==0 and not(sent or routed>0)then setStatus(\"idle\",\"No newer signed release is available\")elseif cloudOffers>0 then setStatus(\"offers\",tostring(cloudOffers)..\" signed cloud release offer\"..(cloudOffers==1 and\"\"or\"s\")..\" available\")end record(\"check\",{cloud=cloudOffers,direct=sent==true,routed=routed,cloudError=cloudErr,error=(not sent)and err or nil})save()if cloudErr and not sent and routed==0 then return false,cloudErr end return true,{cloud=cloudOffers,direct=sent==true,routed=routed,cloudError=cloudErr}end function M.download(source)return beginDownload(source)end function M.installLatest()local checked,err=M.check()if not checked then return false,err end local rows=offers()if#rows==0 then return false,\"no newer compatible signed release is available\"end table.sort(rows,function(a,b)local comparison=release.compareVersions(a.version,b.version)or 0 return comparison==0 and tostring(a.id)<tostring(b.id)or comparison>0 end)return beginDownload(rows[1].id)end function M.cancel()local value=loadState()if not value.transfer then return false,\"no update transfer is active\"end record(\"download_cancelled\",{id=value.transfer.source,version=value.transfer.version})value.transfer=nil;removePart();setStatus(\"cancelled\",\"Update download cancelled\");save()return true end function M.clearCache()local value=loadState()if value.transfer then return false,\"cancel the active download first\"end release.clear();cached=nil;value.cache=nil;setStatus(\"idle\",\"Verified release cache cleared\");record(\"cache_cleared\");save()return true end function M.prepareApply()local package,metadata=release.cachedPackage()if not package then return false,metadata or\"no verified signed release is cached\"end local compare=release.compareVersions(package.manifest.version,version.string)if not compare or compare<=0 then return false,\"cached release is not newer than this CeetOS installation\"end if package.manifest.profile~=profile.id()then return false,\"cached release profile does not match this installation\"end local marker={schema=2,state=\"apply\",profile=package.manifest.profile,version=package.manifest.version,digest=package.manifest.digest,keyId=package.manifest.key_id,approvedAt=now()}store.write(release.PENDING_FILE,marker)local value=loadState();setStatus(\"applying\",\"Applying verified release \"..package.manifest.version..\" after reboot\");record(\"apply_approved\",{version=package.manifest.version,digest=package.manifest.digest:sub(1,16),keyId=package.manifest.key_id});save()return true,{version=package.manifest.version,fingerprint=package.manifest.digest:sub(1,16),keyId=package.manifest.key_id}end function M.recover()local marker=store.read(release.PENDING_FILE,nil)if type(marker)~=\"table\"or marker.state~=\"verify\"then return end local value=loadState()if marker.version==version.string and(marker.profile==nil or marker.profile==profile.id())then if fs.exists(release.PENDING_FILE)then fs.delete(release.PENDING_FILE)end setStatus(\"applied\",\"Verified release \"..version.string..\" is now active\")record(\"apply_verified\",{version=version.string})else setStatus(\"error\",\"Update installed but running version did not match \"..tostring(marker.version))record(\"apply_version_mismatch\",{expected=marker.version,actual=version.string})end save()end function M.tick()local value,timestamp=loadState(),now()local offer=providerOffer()if offer and timestamp-lastBeacon>=BEACON_INTERVAL then net.publicBroadcast(\"update_offer\",offer)lastBeacon=timestamp end local transfer=value.transfer if transfer and transfer.awaiting and timestamp-(transfer.requestedAt or timestamp)>=REQUEST_TIMEOUT then transfer.awaiting,transfer.retries=false,(transfer.retries or 0)+1 if transfer.retries>REQUEST_LIMIT then record(\"download_timeout\",{id=transfer.source,version=transfer.version})value.transfer=nil;removePart();setStatus(\"error\",\"Update source timed out\");save()else local sent,err=transferRequest(transfer)if not sent then value.transfer=nil;removePart();setStatus(\"error\",err);record(\"download_failed\",{id=transfer.source,error=err});save()end end end end function M.status()local value=loadState()return{profile=profile.id(),version=version.string,status=value.status,message=value.message,revision=value.revision,offers=offers(),transfer=value.transfer,cache=value.cache,history=value.history,cacheRoot=release.CACHE_ROOT,maxPackageBytes=release.MAX_PACKAGE_BYTES,}end return M",
  ["ceetos/lib/release_broker.lua"] = "local store=require(\"ceetos.lib.store\")local password=require(\"ceetos.lib.password\")local crypto=require(\"ceetos.lib.mesh_crypto\")local cloud=require(\"ceetos.lib.cloud\")local net=require(\"ceetos.lib.net\")local profile=require(\"ceetos.lib.profile\")local M={}local PATH,LIMIT,LIFE=\"/ceetos/data/release-broker.lua\",8,120000 local pending={}local function now()return os.epoch(\"utc\")end local function localId()return tostring(os.getComputerID())end local function localLabel()return os.getComputerLabel()or(\"Computer \"..localId())end local function clean(v,n)return type(v)==\"string\"and#v>0 and#v<=n and not v:find(\"[%z\\1-\\31\\127]\")end local function state()local value=store.read(PATH,{})return{schema=1,enabled=value.enabled==true,promotedAt=tonumber(value.promotedAt)}end local function save(value)return store.write(PATH,value)end local function key(session,target,targetNonce,phrase)return password.hmacSha256(phrase,table.concat({\"ceetos/release-broker/v1\",session,target,localId(),targetNonce},\"|\"))end local function mac(secret,label,value)return password.hmacSha256(secret,label..\"|\"..value)end local function expiry()return now()+LIFE end local function pendingCount()local count=0 for _,row in pairs(pending)do if row.expires>now()then count=count+1 end end return count end function M.status()local value,cloudState=state(),cloud.publicStatus()local rows={}for id,row in pairs(pending)do if row.expires>now()then rows[#rows+1]={id=id,target=row.target,label=row.label,profile=row.profile,expires=row.expires,state=row.state}end end table.sort(rows,function(a,b)return a.expires<b.expires end)return{enabled=value.enabled,enrolled=cloudState.enrolled,pending=rows}end function M.promote(ownerKey)if not clean(ownerKey,256)then return false,\"owner enrollment key is required\"end local result,err=cloud.call(\"auth\",\"/v1/broker/promote\",{ownerKey=ownerKey})if not result then return false,err end save({schema=1,enabled=true,promotedAt=now()})return true,result end function M.revoke(ownerKey)if not clean(ownerKey,256)then return false,\"owner enrollment key is required\"end local result,err=cloud.call(\"auth\",\"/v1/broker/revoke\",{ownerKey=ownerKey})if not result then return false,err end save({schema=1,enabled=false})for id in pairs(pending)do pending[id]=nil end return true,result end function M.handle(packet)local body,source=packet.body or{},tostring(packet.from or\"\")if packet.type==\"broker_query\"then if not state().enabled or not cloud.publicStatus().enrolled then return end if not clean(body.session,96)or not clean(body.nonce,128)or not clean(body.profile,32)or not clean(body.label,96)or body.profile~=profile.id()then return end if pendingCount()>=LIMIT and not pending[body.session]then return end pending[body.session]={session=body.session,target=source,targetNonce=body.nonce,profile=body.profile,label=body.label,expires=expiry(),state=\"waiting\"}return net.publicSend(source,\"broker_offer\",{session=body.session,broker=localId(),label=localLabel(),profile=profile.id(),expires=pending[body.session].expires})elseif packet.type==\"broker_proof\"then local row=pending[body.session]if not row or row.expires<=now()or row.target~=source or row.state~=\"challenged\"or not clean(body.proof,128)then return end if not password.constantTimeEqual(row.expected,body.proof)then pending[body.session]=nil;return net.publicSend(source,\"broker_reject\",{session=body.session,reason=\"phrase proof rejected\"})end local result,err=cloud.call(\"auth\",\"/v1/broker/enroll\",{session=row.session,targetComputerId=row.target,targetLabel=row.label,targetProfile=row.profile,expires=row.expires})if not result or type(result.node)~=\"table\"then pending[body.session]=nil;return net.publicSend(source,\"broker_reject\",{session=body.session,reason=tostring(err or\"cloud provisioning failed\"):sub(1,96)})end local aad=\"ceetos/release-broker/grant/v1|\"..row.session..\"|\"..row.target..\"|\"..localId()local nonce=crypto.nonce(row.key,1,aad)local envelope=nonce and crypto.seal(row.key,nonce,aad,textutils.serialise({node=result.node,urls=cloud.publicStatus().urls}))pending[body.session]=nil if not envelope then return net.publicSend(source,\"broker_reject\",{session=body.session,reason=\"could not secure enrollment\"})end return net.publicSend(source,\"broker_grant\",{session=body.session,broker=localId(),envelope=envelope})end end function M.approve(session,phrase)local row=pending[tostring(session or\"\")]if not row or row.expires<=now()then return false,\"bootstrap session is unavailable or expired\"end if not clean(phrase,128)then return false,\"invalid bootstrap phrase\"end row.key=key(row.session,row.target,row.targetNonce,phrase)row.brokerNonce=password.newSalt(\"broker:\"..row.session)row.expected=mac(row.key,\"proof\",row.brokerNonce)row.state,row.expires=\"challenged\",expiry()local challenge=mac(row.key,\"challenge\",row.brokerNonce)local sent,err=net.publicSend(row.target,\"broker_challenge\",{session=row.session,broker=localId(),nonce=row.brokerNonce,mac=challenge,expires=row.expires})if not sent then row.state=\"waiting\";return false,err end return true end function M.tick()for id,row in pairs(pending)do if row.expires<=now()then pending[id]=nil end end end return M",
  ["ceetos/lib/broker_bootstrap.lua"] = "local password=require(\"ceetos.lib.password\")local crypto=require(\"ceetos.lib.mesh_crypto\")local M={}local CHANNEL,LIFE=45731,120 local function label()return os.getComputerLabel()or(\"Computer \"..tostring(os.getComputerID()))end local function valid(v,n)return type(v)==\"string\"and#v>0 and#v<=n end local function modem()for _,name in ipairs(peripheral.getNames())do if peripheral.getType(name)==\"modem\"then local wrapped=peripheral.wrap(name)if wrapped and wrapped.isWireless and wrapped.isWireless()then return wrapped end end end end local function random(context)return password.newSalt(context..\":\"..tostring(os.epoch(\"utc\")))end function M.enroll(profile)local link=modem()if not link then return nil,\"No wireless modem is attached; a trusted release broker must be directly reachable.\"end link.open(CHANNEL)local session,target,targetNonce=random(\"session\"),tostring(os.getComputerID()),random(\"target\")local phrase=random(\"phrase\"):sub(1,20)print(\"Trusted broker enrollment required.\")print(\"On an enrolled broker run: nx broker approve \"..session..\" \"..phrase)link.transmit(CHANNEL,CHANNEL,{protocol=\"ceetos/update/1\",type=\"broker_query\",from=target,to=\"0\",body={session=session,nonce=targetNonce,profile=profile,label=label()}})local deadline,broker,linkKey=os.epoch(\"utc\")+LIFE*1000,nil,nil while os.epoch(\"utc\")<deadline do local timer=os.startTimer(2)local event={os.pullEventRaw()}if event[1]==\"timer\"and event[2]==timer then link.transmit(CHANNEL,CHANNEL,{protocol=\"ceetos/update/1\",type=\"broker_query\",from=target,to=\"0\",body={session=session,nonce=targetNonce,profile=profile,label=label()}})elseif event[1]==\"modem_message\"and event[3]==CHANNEL and type(event[5])==\"table\"then local packet,body=event[5],event[5].body or{}if packet.protocol==\"ceetos/update/1\"and tostring(packet.to)==target and body.session==session then if packet.type==\"broker_offer\"then broker=tostring(body.broker)elseif packet.type==\"broker_challenge\"and broker and tostring(body.broker)==broker and valid(body.nonce,128)and valid(body.mac,128)then linkKey=password.hmacSha256(phrase,table.concat({\"ceetos/release-broker/v1\",session,target,broker,targetNonce},\"|\"))if password.constantTimeEqual(password.hmacSha256(linkKey,\"challenge|\"..body.nonce),body.mac)then link.transmit(CHANNEL,CHANNEL,{protocol=\"ceetos/update/1\",type=\"broker_proof\",from=target,to=broker,body={session=session,proof=password.hmacSha256(linkKey,\"proof|\"..body.nonce)}})end elseif packet.type==\"broker_grant\"and linkKey and tostring(body.broker)==broker then local aad=\"ceetos/release-broker/grant/v1|\"..session..\"|\"..target..\"|\"..broker local opened=crypto.open(linkKey,body.envelope,aad)local grant=opened and textutils.unserialise(opened)if type(grant)==\"table\"and type(grant.node)==\"table\"and valid(grant.node.id,96)and valid(grant.node.secret,192)then return grant end elseif packet.type==\"broker_reject\"then return nil,tostring(body.reason or\"broker rejected enrollment\")end end end end return nil,broker and\"Broker did not complete enrollment in time.\"or\"No trusted release broker answered. Installation was not changed.\"end return M",
  ["ceetos/lib/mesh_crypto.lua"] = "local password=require(\"ceetos.lib.password\")local M={}local band,bor,bxor=bit32.band,bit32.bor,bit32.bxor local lshift,rshift=bit32.lshift,bit32.rshift local MOD32,LIMB=4294967296,67108864 local function add32(...)local n=0 for i=1,select(\"#\",...)do n=(n+(select(i,...)or 0))%MOD32 end return n end local function rotl(v,bits)return bor(lshift(v,bits),rshift(v,32-bits))end local function le32(data,index)local a,b,c,d=data:byte(index,index+3)return(a or 0)+(b or 0)*256+(c or 0)*65536+(d or 0)*16777216 end local function put32(value)value=value%MOD32 return string.char(value%256,math.floor(value/256)%256,math.floor(value/65536)%256,math.floor(value/16777216)%256)end local function le64(value)value=math.max(0,math.floor(tonumber(value)or 0))local low,high=value%MOD32,math.floor(value/MOD32)%MOD32 return put32(low)..put32(high)end local function hexToRaw(value)if type(value)~=\"string\"or#value%2~=0 or value:find(\"[^%x]\")then return nil end return(value:gsub(\"..\",function(pair)return string.char(tonumber(pair,16))end))end local function rawToHex(value)return(value:gsub(\".\",function(byte)return string.format(\"%02x\",byte:byte())end))end local function xorBytes(left,right)local out={}for i=1,#left do out[i]=string.char(bxor(left:byte(i),right:byte(i)))end return table.concat(out)end local function quarter(x,a,b,c,d)x[a]=add32(x[a],x[b]);x[d]=rotl(bxor(x[d],x[a]),16)x[c]=add32(x[c],x[d]);x[b]=rotl(bxor(x[b],x[c]),12)x[a]=add32(x[a],x[b]);x[d]=rotl(bxor(x[d],x[a]),8)x[c]=add32(x[c],x[d]);x[b]=rotl(bxor(x[b],x[c]),7)end local function chachaBlock(key,counter,nonce)local state={0x61707865,0x3320646e,0x79622d32,0x6b206574}for i=1,8 do state[4+i]=le32(key,(i-1)*4+1)end state[13],state[14],state[15],state[16]=counter%MOD32,le32(nonce,1),le32(nonce,5),le32(nonce,9)local x={}for i=1,16 do x[i]=state[i]end for _=1,10 do quarter(x,1,5,9,13);quarter(x,2,6,10,14);quarter(x,3,7,11,15);quarter(x,4,8,12,16)quarter(x,1,6,11,16);quarter(x,2,7,12,13);quarter(x,3,8,9,14);quarter(x,4,5,10,15)end local out={}for i=1,16 do out[i]=put32(add32(x[i],state[i]))end return table.concat(out)end local function chachaXor(key,nonce,counter,plaintext)local out,position={},1 while position<=#plaintext do local block=chachaBlock(key,counter,nonce)local part=plaintext:sub(position,position+63)out[#out+1]=xorBytes(part,block:sub(1,#part))position,counter=position+#part,(counter+1)%MOD32 end return table.concat(out)end local function poly1305(message,key)local function zero(length)local out={};for i=1,length do out[i]=0 end;return out end local function normalise(value)local carry=0 for i=1,#value do local n=(value[i]or 0)+carry value[i],carry=n%256,math.floor(n/256)end while carry>0 do value[#value+1],carry=carry%256,math.floor(carry/256)end return value end local function add(left,right)local out=zero(math.max(#left,#right)+1)for i=1,#left do out[i]=out[i]+left[i]end for i=1,#right do out[i]=out[i]+right[i]end return normalise(out)end local function multiply(left,right)local out=zero(#left+#right+1)for i=1,#left do for j=1,#right do out[i+j-1]=out[i+j-1]+left[i]*right[j]end end return normalise(out)end local function high130(value)local out={}for i=1,#value-16 do local low=value[i+16]or 0 local high=value[i+17]or 0 out[i]=math.floor(low/4)+(high%4)*64 end return normalise(out)end local function hasHigh(value)if#value>17 then for i=18,#value do if value[i]~=0 then return true end end end return(value[17]or 0)>=4 end local function reduce(value)while hasHigh(value)do local low,high=zero(17),high130(value)for i=1,17 do low[i]=value[i]or 0 end low[17]=low[17]%4 for i=1,#high do high[i]=high[i]*5 end value=add(low,high)end local prime={251}for i=2,16 do prime[i]=255 end prime[17]=3 local greater=false for i=17,1,-1 do if(value[i]or 0)~=prime[i]then greater=(value[i]or 0)>prime[i];break end end if greater then local borrow=0 for i=1,17 do local n=(value[i]or 0)-prime[i]-borrow if n<0 then n,borrow=n+256,1 else borrow=0 end value[i]=n end end return value end local r,h=zero(17),zero(17)for i=1,16 do r[i]=key:byte(i)end r[4],r[8],r[12],r[16]=band(r[4],15),band(r[8],15),band(r[12],15),band(r[16],15)r[5],r[9],r[13]=band(r[5],252),band(r[9],252),band(r[13],252)for at=1,#message,16 do local part,n=message:sub(at,at+15),zero(17)for i=1,#part do n[i]=part:byte(i)end n[#part+1]=1 h=reduce(multiply(add(h,n),r))end local pad,out,carry={},{},0 for i=1,16 do pad[i]=key:byte(i+16)end for i=1,16 do local n=(h[i]or 0)+pad[i]+carry out[i],carry=n%256,math.floor(n/256)end return string.char(table.unpack(out))end local function padded(value)return value..string.rep(\"\\0\",(16-(#value%16))%16)end local function derive(linkKey)local raw=hexToRaw(password.hmacSha256(linkKey,\"ceetos/mesh/aead-key/v1\")or\"\")return raw end function M.nonce(linkKey,counter,context)local raw=hexToRaw(password.hmacSha256(linkKey,\"ceetos/mesh/aead-nonce/v1|\"..tostring(counter)..\"|\"..tostring(context or\"\"))or\"\")return raw and raw:sub(1,12)or nil end function M.seal(linkKey,nonce,aad,plaintext)if type(linkKey)~=\"string\"or type(nonce)~=\"string\"or#nonce~=12 or type(aad)~=\"string\"or type(plaintext)~=\"string\"then return nil,\"invalid mesh envelope input\"end local key=derive(linkKey);if not key or#key~=32 then return nil,\"could not derive mesh envelope key\"end local polyKey=chachaBlock(key,0,nonce):sub(1,32)local ciphertext=chachaXor(key,nonce,1,plaintext)local tag=poly1305(padded(aad)..padded(ciphertext)..le64(#aad)..le64(#ciphertext),polyKey)return{nonce=rawToHex(nonce),ciphertext=rawToHex(ciphertext),tag=rawToHex(tag)}end function M.open(linkKey,envelope,aad)if type(linkKey)~=\"string\"or type(envelope)~=\"table\"or type(aad)~=\"string\"then return nil,\"invalid mesh envelope\"end local nonce,ciphertext,tag=hexToRaw(envelope.nonce or\"\"),hexToRaw(envelope.ciphertext or\"\"),hexToRaw(envelope.tag or\"\")if not nonce or#nonce~=12 or not ciphertext or not tag or#tag~=16 then return nil,\"invalid mesh envelope encoding\"end local key=derive(linkKey);if not key or#key~=32 then return nil,\"could not derive mesh envelope key\"end local polyKey=chachaBlock(key,0,nonce):sub(1,32)local expected=rawToHex(poly1305(padded(aad)..padded(ciphertext)..le64(#aad)..le64(#ciphertext),polyKey))if not password.constantTimeEqual(expected,envelope.tag)then return nil,\"mesh envelope authentication failed\"end return chachaXor(key,nonce,1,ciphertext)end function M.selfTest()local key=hexToRaw(\"85d6be7857556d337f4452fe42d506a80103808afb0db2fd4abff6af4149f51b\")local tag=poly1305(\"Cryptographic Forum Research Group\",key)return rawToHex(tag)==\"a8061dc1305136c6c22b8baf0c0127a9\"end return M",
  ["ceetos/lib/audit.lua"] = "local store=require(\"ceetos.lib.store\")local M={}local PATH=\"/ceetos/data/audit.log\"M.LIMITS={maxEntries=250,maxBytes=64*1024}function M.log(actor,action,detail)return store.append(PATH,{ts=os.epoch(\"utc\"),actor=actor or\"system\",action=action,detail=detail,},M.LIMITS)end function M.recent(limit)limit=math.max(1,math.min(50,tonumber(limit)or 10))local raw=store.readAppend(PATH)local result={}for line in(raw..\"\\n\"):gmatch(\"(.-)\\n\")do if line~=\"\"then local entry=textutils.unserialise(line)if entry then result[#result+1]=entry if#result>limit then table.remove(result,1)end end end end return result end return M",
  ["ceetos/lib/net.lua"] = "local store=require(\"ceetos.lib.store\")local audit=require(\"ceetos.lib.audit\")local password=require(\"ceetos.lib.password\")local meshCrypto=require(\"ceetos.lib.mesh_crypto\")local M={handler=nil,publicHandler=nil}local PATH=\"/ceetos/data/network.lua\"local SECURITY_PATH=\"/ceetos/data/network-security.lua\"local SEEN_PATH=\"/ceetos/data/seen-peers.lua\"local CHANNEL,PROTOCOL=45731,\"ceetos/2\"local PUBLIC_UPDATE_PROTOCOL=\"ceetos/update/1\"local PUBLIC_UPDATE_TYPES={update_query=true,update_offer=true,update_chunk_request=true,update_chunk=true,broker_query=true,broker_offer=true,broker_challenge=true,broker_proof=true,broker_grant=true,broker_reject=true,}local DISCOVERY_CHANNEL=CHANNEL local DISCOVERY_TTL,ROUTE_TTL,PACKET_TTL=300000,120000,8 local DISCOVERY_INTERVAL,PAIR_RETRY,ROUTE_INTERVAL=2000,2000,15000 local MESH_BEACON_INTERVAL,MESH_FULL_INTERVAL=15000,30000 local PROBE_INTERVAL,PROBE_DEGRADED,PROBE_OFFLINE,PROBE_RECOVERY=5000,3,6,10000 local ROUTE_CANDIDATE_LIMIT,ROUTE_BUCKET_LIMIT=4,96 local DISCOVERY_CANDIDATE_LIMIT,SEEN_PACKET_LIMIT=32,1024 local SEEN_PEER_LIMIT,SEEN_PEER_TTL=128,24*60*60*1000 local SAFE_READ_WINDOW,SAFE_READ_LIMIT=1000,16 local MAX_PACKET_STRING,MAX_PACKET_TABLE,MAX_PACKET_DEPTH=8192,128,6 local COUNTER_RESERVATION,MAX_RX_EPOCHS,MAX_REVERSE_ROUTES=64,3,128 local REPLAY_WINDOW,MAX_COUNTER_JUMP=64,4096 local routes,discovered,confirmations,seenPackets,reverseRoutes={},{},{},{},{}local routeSequence,lastRouteAdvertisement,routesDirty=0,0,false local routeSession local discovery,lastDiscovery=nil,{state=\"inactive\"}local configCache,modemCache,modemCacheAt=nil,nil,0 local security={tx={},rx={}}local autoEdges,linkHealth,meshOffers,meshSeen,advertisedRoutes,meshEnrollmentSends={},{},{},{},{},{}local lastMeshBeacon,lastMeshFull,meshDirty=0,0,false local transport={modem=false,wireless=false,name=nil,channels={},lastTransmit=nil,lastReceive=nil,lastDistance=nil,lastError=nil,rejected=0,requestAdmissions=0,requestDuplicates=0,requestEvictions=0,}local traces={discovery={},routing={},transport={}}local TRACE_LIMIT=48 local MESH_STATE_LIMIT=128 local rejectPacket local function now()return os.epoch(\"utc\")end local function id()return tostring(os.getComputerID())end local function label()return os.getComputerLabel()or(\"Computer \"..id())end routeSession=password.newSalt(\"route-session:\"..id())local function isBroadcast(value)return tostring(value)==\"0\"end local function isForUs(value)return tostring(value)==id()end local function copy(value,depth)if type(value)~=\"table\"then return value end if(depth or 0)>MAX_PACKET_DEPTH then return nil end local out={}for k,v in pairs(value)do out[k]=copy(v,(depth or 0)+1)end return out end local function trace(kind,phase,fields)local bucket=traces[kind]or traces.transport local row={time=now(),phase=phase}for key,value in pairs(fields or{})do if key~=\"phrase\"and key~=\"proof\"and key~=\"key\"and key~=\"secret\"and key~=\"mac\"and key~=\"packet\"and key~=\"tag\"then row[key]=value end end bucket[#bucket+1]=row while#bucket>TRACE_LIMIT do table.remove(bucket,1)end end local function read(path,fallback)return store.read(path,fallback)end local function config()local value=configCache or read(PATH,{peers={},channel=CHANNEL})value.peers=type(value.peers)==\"table\"and value.peers or{}value.channel=tonumber(value.channel)or CHANNEL configCache=value return value end local function save(value)configCache=value local ok,err=store.write(PATH,value)if ok==false then error(err or\"could not save network configuration\",0)end end local function migrateV1Links()local value,changed=config(),false for _,peer in pairs(value.peers)do if peer.protocol~=2 or peer.rekeyRequired or type(peer.key)~=\"string\"or#peer.key<32 then peer.protocol,peer.rekeyRequired,peer.key=1,true,nil peer.secret=nil changed=true end end if changed then save(value)end return value end local function activePeer(value,peerId)local peer=value.peers[tostring(peerId)]if peer and peer.protocol==2 and peer.rekeyRequired~=true and type(peer.key)==\"string\"and#peer.key>=32 then return peer end return nil end local function activeLink(value,peerId)local manual=activePeer(value,peerId)if manual then return manual,\"direct\"end local edge=autoEdges[tostring(peerId)]if edge and type(edge.key)==\"string\"and#edge.key>=32 and(edge.expires or 0)>now()then return edge,\"mesh\"end return nil end function M.isDirectPeer(peerId)return activePeer(config(),tostring(peerId))~=nil end function M.isTrustedDirectLink(peerId)return activeLink(config(),tostring(peerId))~=nil end local function healthFor(peerId)peerId=tostring(peerId)local state=linkHealth[peerId]if not state then state={state=\"healthy\",successes=0,misses=0,lastAuthenticated=now(),stableSince=now(),inFlight=0}linkHealth[peerId]=state end return state end local function linkState(peerId)return healthFor(peerId).state or\"healthy\"end local function currentInFlight(state,timestamp)state.inFlightUntil=type(state.inFlightUntil)==\"table\"and state.inFlightUntil or{}local kept={}for _,expires in ipairs(state.inFlightUntil)do if expires>timestamp then kept[#kept+1]=expires end end state.inFlightUntil,state.inFlight=kept,#kept return state.inFlight end local function markInFlight(state)local timestamp=now()currentInFlight(state,timestamp)state.inFlightUntil[#state.inFlightUntil+1]=timestamp+SAFE_READ_WINDOW table.sort(state.inFlightUntil)while#state.inFlightUntil>SAFE_READ_LIMIT do table.remove(state.inFlightUntil,1)end state.inFlight=#state.inFlightUntil end local function linkUsable(value,peerId,allowDegraded)if not activeLink(value,peerId)then return false end local state=linkState(peerId)return state==\"healthy\"or(allowDegraded and state==\"degraded\")end local function touchLink(peerId,distance,source)local state,timestamp=healthFor(peerId),now()state.lastAuthenticated,state.lastDistance,state.lastSource=timestamp,distance,source or\"traffic\"state.misses,state.awaiting,state.probeToken=0,false,nil state.loss=math.max(0,(state.loss or 0)*0.5)if state.state~=\"healthy\"then state.successes=(state.successes or 0)+1 if state.successes>=2 then state.state,state.stableSince,state.recoveredAt=\"healthy\",timestamp,timestamp routesDirty=true trace(\"routing\",\"link_recovered\",{peer=peerId,source=source})end else state.successes=math.min(2,(state.successes or 0)+1)end end local function allDirectLinks(value)local out,seen={},{}for peerId in pairs(value.peers)do if activePeer(value,peerId)then out[#out+1],seen[peerId]=tostring(peerId),true end end for peerId,edge in pairs(autoEdges)do if not seen[peerId]and edge.expires>now()then out[#out+1]=tostring(peerId)end end table.sort(out,function(a,b)return tonumber(a)<tonumber(b)end)return out end local function remember(peer,peerLabel,extra)local value=read(SEEN_PATH,{})local timestamp,retained=now(),{}for key,item in pairs(value)do local seen=type(item)==\"table\"and tonumber(item.seen)or nil if not seen or seen+SEEN_PEER_TTL<=timestamp then value[key]=nil else retained[#retained+1]={key=key,seen=seen}end end table.sort(retained,function(a,b)return a.seen<b.seen end)local limit=value[tostring(peer)]and SEEN_PEER_LIMIT or SEEN_PEER_LIMIT-1 while#retained>limit do value[table.remove(retained,1).key]=nil end local old=value[tostring(peer)]or{}old.label,old.seen=peerLabel or old.label or(\"Computer \"..tostring(peer)),timestamp if extra then for key,item in pairs(extra)do if key~=\"tag\"and key~=\"proof\"then old[key]=item end end end value[tostring(peer)]=old store.write(SEEN_PATH,value)end local function admitSeenPacket(requestId)local timestamp,oldestKey,oldestAt,count=now(),nil,nil,0 for key,seenAt in pairs(seenPackets)do if seenAt+ROUTE_TTL<=timestamp then seenPackets[key]=nil else count=count+1 if not oldestAt or seenAt<oldestAt then oldestKey,oldestAt=key,seenAt end end end if seenPackets[requestId]then transport.requestDuplicates=transport.requestDuplicates+1;return false end if count>=SEEN_PACKET_LIMIT and oldestKey then seenPackets[oldestKey],transport.requestEvictions=nil,transport.requestEvictions+1 end seenPackets[requestId]=timestamp transport.requestAdmissions=transport.requestAdmissions+1 return true end local function modem(requireWireless)local candidates,used={},{}local sides=rs and rs.getSides and rs.getSides()or{}for _,name in ipairs(sides)do if peripheral.getType(name)==\"modem\"and not used[name]then candidates[#candidates+1],used[name]=name,true end end if peripheral.getNames then for _,name in ipairs(peripheral.getNames())do if peripheral.getType(name)==\"modem\"and not used[name]then candidates[#candidates+1],used[name]=name,true end end end local fallback for _,name in ipairs(candidates)do local wrapped=peripheral.wrap(name)if wrapped then fallback=fallback or wrapped local ok,wireless=pcall(function()return not wrapped.isWireless or wrapped.isWireless()end)if ok and wireless then return wrapped end end end if requireWireless then return nil end return fallback end local function modems()if modemCache and now()-modemCacheAt<1000 then return modemCache end local out,seen={},{}local function add(name)if not seen[name]and peripheral.getType(name)==\"modem\"then local wrapped=peripheral.wrap(name)if wrapped then out[#out+1],seen[name]={name=name,modem=wrapped},true end end end for _,name in ipairs(rs and rs.getSides and rs.getSides()or{})do add(name)end for _,name in ipairs(peripheral.getNames and peripheral.getNames()or{})do add(name)end modemCache,modemCacheAt=out,now()return out end function M.invalidateTransport()modemCache,modemCacheAt=nil,0 end local function wirelessModems()local out={}for _,item in ipairs(modems())do local ok,wireless=pcall(function()return not item.modem.isWireless or item.modem.isWireless()end)if ok and wireless then out[#out+1]=item end end return out end local function openChannels(value)local channels={value.channel or CHANNEL}if channels[1]~=DISCOVERY_CHANNEL then channels[#channels+1]=DISCOVERY_CHANNEL end transport.channels=channels local all=modems()if#all==0 then transport.lastError=\"no modem attached\";trace(\"transport\",\"modem_missing\");return channels end for _,item in ipairs(all)do for _,channel in ipairs(channels)do local ok,err=pcall(item.modem.open,channel)if not ok then transport.lastError=tostring(err)trace(\"transport\",\"channel_open_failed\",{channel=channel,error=transport.lastError,modem=item.name})end end end return channels end local function canonical(value,depth)depth=depth or 0 local kind=type(value)if kind==\"nil\"then return\"n\"end if kind==\"boolean\"then return value and\"b1\"or\"b0\"end if kind==\"number\"then if value~=value or value==math.huge or value==-math.huge then return nil,\"invalid numeric packet value\"end return\"d\"..string.format(\"%.17g\",value)..\";\"end if kind==\"string\"then if#value>MAX_PACKET_STRING then return nil,\"packet string exceeds limit\"end return\"s\"..tostring(#value)..\":\"..value end if kind~=\"table\"then return nil,\"unsupported packet value\"end if depth>=MAX_PACKET_DEPTH then return nil,\"packet nesting exceeds limit\"end local entries={}for key,item in pairs(value)do if#entries>=MAX_PACKET_TABLE then return nil,\"packet table exceeds limit\"end local encodedKey,keyErr=canonical(key,depth+1);if not encodedKey then return nil,keyErr end local encodedValue,valueErr=canonical(item,depth+1);if not encodedValue then return nil,valueErr end entries[#entries+1]=encodedKey..\"=\"..encodedValue end table.sort(entries)return\"t\"..tostring(#entries)..\"{\"..table.concat(entries,\",\")..\"}\"end local function packetMaterial(packet)return canonical({protocol=packet.protocol,type=packet.type,from=packet.from,to=packet.to,origin=packet.origin,destination=packet.destination,ttl=packet.ttl,request_id=packet.request_id,timestamp=packet.timestamp,reply_to=packet.reply_to,auth=packet.auth and{epoch=packet.auth.epoch,counter=packet.auth.counter}or nil,confidential=packet.confidential==true,body=packet.body,})end local function confidentialAad(packet)return table.concat({\"ceetos/confidential/v1\",tostring(packet.type),tostring(packet.origin),tostring(packet.destination),tostring(packet.request_id)},\"|\")end local function sealBody(peer,packet)local encoded=textutils.serialise(packet.body or{},{compact=true})if#encoded>6144 then return nil,\"confidential payload exceeds limit\"end local nonce=meshCrypto.nonce(peer.key,password.newSalt(\"confidential:\"..tostring(packet.request_id)),confidentialAad(packet))if not nonce then return nil,\"could not create confidential nonce\"end local envelope,err=meshCrypto.seal(peer.key,nonce,confidentialAad(packet),encoded)if not envelope then return nil,err end return{envelope=envelope}end local function openBody(peer,packet)if type(packet.body)~=\"table\"or type(packet.body.envelope)~=\"table\"then return nil,\"malformed confidential envelope\"end local decoded,err=meshCrypto.open(peer.key,packet.body.envelope,confidentialAad(packet))if not decoded or#decoded>6144 then return nil,err or\"invalid confidential payload\"end local ok,value=pcall(textutils.unserialise,decoded)if not ok or type(value)~=\"table\"then return nil,\"invalid confidential payload\"end return value end local function loadSecurity()security=read(SECURITY_PATH,{tx={},rx={}})security.tx=type(security.tx)==\"table\"and security.tx or{}security.rx=type(security.rx)==\"table\"and security.rx or{}end local function saveSecurity()local ok,err=store.write(SECURITY_PATH,security)if ok==false then error(err or\"could not persist peer replay state\",0)end end local function counter(peerId)local key=tostring(peerId)local tx=security.tx[key]if not tx then tx={epoch=password.newSalt(\"peer-tx:\"..key),reserved=0};security.tx[key]=tx end tx.next=tonumber(tx.next)or((tonumber(tx.reserved)or 0)+1)if tx.next>(tonumber(tx.reserved)or 0)then tx.reserved=tx.next+COUNTER_RESERVATION-1 saveSecurity()end local value=tx.next;tx.next=value+1 return tx.epoch,value end local function verifyReplay(from,auth)if type(auth)~=\"table\"or type(auth.epoch)~=\"string\"or#auth.epoch<16 or#auth.epoch>128 then return false,\"invalid packet epoch\"end local sequence=tonumber(auth.counter)if not sequence or sequence<1 or sequence%1~=0 then return false,\"invalid packet counter\"end local key=tostring(from)local row=security.rx[key]or{epochs={}}row.epochs=type(row.epochs)==\"table\"and row.epochs or{}local epochState=row.epochs[auth.epoch]if type(epochState)~=\"table\"then local legacy=tonumber(epochState)epochState={max=legacy or 0,seen={}}if legacy and legacy>0 then epochState.seen[tostring(legacy)]=true end end epochState.max=math.max(0,math.floor(tonumber(epochState.max)or 0))epochState.seen=type(epochState.seen)==\"table\"and epochState.seen or{}if epochState.max>0 and sequence>epochState.max+MAX_COUNTER_JUMP then return false,\"packet counter jump requires rekey or resynchronization\"end if sequence<=epochState.max-REPLAY_WINDOW then return false,\"stale packet counter\"end local sequenceKey=tostring(sequence)if epochState.seen[sequenceKey]then return false,\"replayed packet counter\"end if sequence>epochState.max then epochState.max=sequence end epochState.seen[sequenceKey]=true for seenCounter in pairs(epochState.seen)do local numeric=tonumber(seenCounter)if not numeric or numeric<=epochState.max-REPLAY_WINDOW then epochState.seen[seenCounter]=nil end end row.epochs[auth.epoch]=epochState local order={}for epoch,epochValue in pairs(row.epochs)do local maximum=type(epochValue)==\"table\"and tonumber(epochValue.max)or tonumber(epochValue)order[#order+1]={epoch=epoch,max=maximum or 0}end table.sort(order,function(a,b)return a.max>b.max end)while#order>MAX_RX_EPOCHS do row.epochs[table.remove(order).epoch]=nil end security.rx[key]=row saveSecurity()return true end local function newRequestId()return id()..\":\"..password.newSalt(\"peer-request\")end local function sign(peerId,peer,packet)local epoch,sequence=counter(peerId)packet.auth={epoch=epoch,counter=sequence}local material,err=packetMaterial(packet)if not material then return false,err end local mac,macErr=password.hmacSha256(peer.key,material)if not mac then return false,macErr end packet.auth.mac=mac return true end local function verifyPacket(packet,value)if type(packet)~=\"table\"then return false,\"packet is not a table\"end if packet.protocol~=PROTOCOL then return false,packet.protocol==\"ceetos/1\"and\"v1 packet rejected; rekey required\"or\"invalid packet protocol\"end if type(packet.type)~=\"string\"or#packet.type==0 or#packet.type>64 then return false,\"invalid packet type\"end if type(packet.from)~=\"string\"and type(packet.from)~=\"number\"then return false,\"invalid packet sender\"end local from=tostring(packet.from)local peer=activeLink(value,from)if not peer then return false,\"sender is not an active direct or mesh peer\"end if not isForUs(packet.to)then return false,\"packet addressed to another hop\"end if type(packet.origin)~=\"string\"and type(packet.origin)~=\"number\"then return false,\"invalid packet origin\"end if type(packet.destination)~=\"string\"and type(packet.destination)~=\"number\"then return false,\"invalid packet destination\"end if type(packet.request_id)~=\"string\"or#packet.request_id==0 or#packet.request_id>128 then return false,\"invalid packet request ID\"end if packet.reply_to~=nil and(type(packet.reply_to)~=\"string\"or#packet.reply_to==0 or#packet.reply_to>128)then return false,\"invalid reply correlation ID\"end local ttl=tonumber(packet.ttl)if not ttl or ttl%1~=0 or ttl<1 or ttl>PACKET_TTL then return false,\"invalid packet TTL\"end local timestamp=tonumber(packet.timestamp)if not timestamp or math.abs(now()-timestamp)>ROUTE_TTL then return false,\"stale packet timestamp\"end if type(packet.auth)~=\"table\"or type(packet.auth.mac)~=\"string\"or#packet.auth.mac~=64 then return false,\"missing packet MAC\"end local receivedMac=packet.auth.mac packet.auth.mac=nil local material,materialErr=packetMaterial(packet)packet.auth.mac=receivedMac if not material then return false,materialErr end local expected,macErr=password.hmacSha256(peer.key,material)if not expected or not password.constantTimeEqual(expected,receivedMac)then return false,macErr or\"invalid packet MAC\"end local replayOk,replayErr=verifyReplay(from,packet.auth)if not replayOk then return false,replayErr end return true,peer end local function routeBucket(targetId)local bucket=routes[tostring(targetId)]if bucket and bucket.next then bucket={candidates={{next=tostring(bucket.next),hops=tonumber(bucket.hops)or PACKET_TTL,expires=tonumber(bucket.expires)or 0,label=bucket.label,mode=bucket.mode,masterId=bucket.masterId,legacy=true,loss=1,}},primary=tostring(bucket.next),changedAt=now(),reason=\"legacy route\"}routes[tostring(targetId)]=bucket end if not bucket then bucket={candidates={},changedAt=now(),reason=\"new\"}routes[tostring(targetId)]=bucket end bucket.candidates=type(bucket.candidates)==\"table\"and bucket.candidates or{}return bucket end local function candidateHealth(candidate)local state=healthFor(candidate.next)local rank=state.state==\"healthy\"and 0 or(state.state==\"degraded\"and 1 or 2)return rank,state end local function directCandidate(value,targetId)local link,kind=activeLink(value,targetId)if link then return{next=targetId,hops=1,expires=now()+ROUTE_TTL,kind=kind,direct=true,label=link.label,mode=link.mode,masterId=link.masterId}end end local function validCandidates(targetId,value)local timestamp,out,seen=now(),{},{}local direct=directCandidate(value,targetId)if direct then out[#out+1],seen[direct.next..\":\"..direct.kind]=direct,true end local bucket=routes[tostring(targetId)]if bucket then for _,candidate in ipairs(routeBucket(targetId).candidates)do local key=tostring(candidate.next)..\":\"..tostring(candidate.kind or\"route\")if not seen[key]and candidate.expires>timestamp and candidate.hops>=1 and candidate.hops<=PACKET_TTL and activeLink(value,candidate.next)then out[#out+1],seen[key]=candidate,true end end end return out end local function compareCandidates(a,b)local ar,ah=candidateHealth(a)local br,bh=candidateHealth(b)if ar~=br then return ar<br end if(a.hops or PACKET_TTL)~=(b.hops or PACKET_TTL)then return(a.hops or PACKET_TTL)<(b.hops or PACKET_TTL)end if(ah.loss or 0)~=(bh.loss or 0)then return(ah.loss or 0)<(bh.loss or 0)end if(ah.rtt or 0)~=(bh.rtt or 0)then return(ah.rtt or 0)<(bh.rtt or 0)end return tostring(a.next)<tostring(b.next)end local function primaryCandidate(targetId,value,safe)local candidates=validCandidates(targetId,value)if#candidates==0 then return nil end table.sort(candidates,compareCandidates)local target,bucket,selected=tostring(targetId),routeBucket(targetId),candidates[1]local existing for _,item in ipairs(candidates)do if tostring(item.next)==tostring(bucket.primary)then existing=item;break end end if existing then local oldRank,newRank=candidateHealth(existing),candidateHealth(selected)if oldRank<newRank then selected=existing elseif oldRank==newRank and oldRank==0 and(selected.hops or 99)<(existing.hops or 99)then local health=healthFor(selected.next)if(health.successes or 0)<2 or(health.recoveredAt and now()-(health.stableSince or now())<PROBE_RECOVERY)then selected=existing end end end if safe then local rank,_,choices=candidateHealth(selected),nil,{}for _,item in ipairs(candidates)do local itemRank=candidateHealth(item)if itemRank==rank and item.hops==selected.hops then choices[#choices+1]=item end end if#choices>1 then table.sort(choices,function(a,b)local ah,bh=healthFor(a.next),healthFor(b.next)local aLoad,bLoad=currentInFlight(ah,now()),currentInFlight(bh,now())if aLoad~=bLoad then return aLoad<bLoad end return tostring(a.next)<tostring(b.next)end)selected=choices[1]end end if bucket.primary~=tostring(selected.next)then bucket.primary,bucket.changedAt,bucket.reason=tostring(selected.next),now(),existing and\"health/hop selection\"or\"initial selection\"trace(\"routing\",\"route_selected\",{destination=target,next=selected.next,hops=selected.hops,reason=bucket.reason})end return selected end local function nextHop(target,safe)local candidate=primaryCandidate(target,config(),safe)if candidate then local health=healthFor(candidate.next)if safe then markInFlight(health)end health.lastSelected=now()return candidate.next,candidate end routes[tostring(target)]=nil end local function transmit(items,channel,packet)if#items==0 then return false,\"no compatible modem attached\"end local sent,lastError=false,nil for _,item in ipairs(items)do local ok,err=pcall(item.modem.transmit,channel,channel,packet)if ok then sent=true else lastError=tostring(err)end end if not sent then transport.lastError=lastError or\"modem transmit failed\"end return sent,transport.lastError end local function send(target,input,forcedHop)local hop=forcedHop or nextHop(target,input.routeSafe==true and input.balanceSafe==true)if not hop then return false,\"peer is unreachable or requires rekey\"end local value,peer=config(),activeLink(config(),hop)if not peer then return false,\"direct route is unavailable\"end local packet=copy(input)packet.auth,packet.protocol,packet.from,packet.to=nil,PROTOCOL,id(),hop packet.origin,packet.destination=tostring(packet.origin or id()),tostring(packet.destination or target)packet.ttl=math.floor(tonumber(packet.ttl)or PACKET_TTL)packet.timestamp=now()if packet.ttl<1 or packet.ttl>PACKET_TTL then return false,\"invalid packet TTL\"end if type(packet.request_id)~=\"string\"or#packet.request_id==0 then packet.request_id=newRequestId()end if packet.confidential==true then local sealed,sealErr=sealBody(peer,packet)if not sealed then return false,sealErr end packet.body=sealed end local signed,signErr=sign(hop,peer,packet)if not signed then return false,signErr end openChannels(value)local links=wirelessModems()if#links==0 then return false,\"no wireless modem attached\"end local sent,err=transmit(links,value.channel or CHANNEL,packet)if not sent then trace(\"transport\",\"transmit_failed\",{type=packet.type,to=hop,error=err});return false,err end transport.lastTransmit,transport.lastError=now(),nil healthFor(hop).lastTransmit=now()trace(\"routing\",\"packet_sent\",{type=packet.type,to=hop,destination=packet.destination,links=#links})return true end local function rememberReverse(packet,previousHop)if type(packet)~=\"table\"or type(packet.request_id)~=\"string\"or#packet.request_id==0 then return false end local requestId,origin=packet.request_id,tostring(packet.origin)local existing=reverseRoutes[requestId]if existing and(existing.previous~=tostring(previousHop)or existing.origin~=origin)then return false,\"reverse route collision\"end reverseRoutes[requestId]={previous=tostring(previousHop),origin=origin,expires=now()+ROUTE_TTL}local entries={}for key,route in pairs(reverseRoutes)do entries[#entries+1]={id=key,expires=route.expires or 0}end table.sort(entries,function(a,b)return a.expires<b.expires end)while#entries>MAX_REVERSE_ROUTES do reverseRoutes[table.remove(entries,1).id]=nil end return true end local function broadcast(packet)local links=wirelessModems()if#links==0 then transport.lastError=\"no wireless modem attached\";trace(\"discovery\",\"wireless_missing\");return false,transport.lastError end local out=copy(packet)out.protocol,out.from,out.to=PROTOCOL,id(),0 out.timestamp=now()openChannels(config())local sent,err=transmit(links,DISCOVERY_CHANNEL,out)if not sent then trace(\"discovery\",\"transmit_failed\",{type=out.type,error=err});return false,err end transport.lastTransmit,transport.lastError=now(),nil trace(\"discovery\",out.type==\"discover\"and\"announcement_sent\"or\"discovery_packet_sent\",{type=out.type})return true end local function publicTransmit(target,typeName,body)if not PUBLIC_UPDATE_TYPES[typeName]then return false,\"invalid public update packet type\"end local encoded,encodeErr=canonical(body or{})if not encoded then return false,encodeErr or\"invalid public update body\"end local links=wirelessModems()if#links==0 then transport.lastError=\"no wireless modem attached\";return false,transport.lastError end openChannels(config())local packet={protocol=PUBLIC_UPDATE_PROTOCOL,type=typeName,from=id(),to=tostring(target),timestamp=now(),request_id=newRequestId(),body=body or{},}local sent,err=transmit(links,DISCOVERY_CHANNEL,packet)if not sent then transport.lastError=err or\"public update transmit failed\"trace(\"transport\",\"public_update_transmit_failed\",{type=typeName,to=target,error=transport.lastError})return false,transport.lastError end transport.lastTransmit,transport.lastError=now(),nil trace(\"transport\",\"public_update_sent\",{type=typeName,to=target,bytes=#encoded})return true end function M.publicSend(target,typeName,body)if tostring(target)==\"0\"or tostring(target)==\"\"then return false,\"public update target must be a computer ID\"end return publicTransmit(target,typeName,body)end function M.publicBroadcast(typeName,body)return publicTransmit(\"0\",typeName,body)end local function handshake(target,packet)local links=wirelessModems()if#links==0 then transport.lastError=\"no wireless modem attached\";return false,transport.lastError end local out=copy(packet)out.protocol,out.from,out.to,out.timestamp=PROTOCOL,id(),tostring(target),now()out.destination,out.ttl=tostring(target),1 openChannels(config())local sent,err=transmit(links,DISCOVERY_CHANNEL,out)if not sent then trace(\"discovery\",\"handshake_transmit_failed\",{type=out.type,to=target,error=err});return false,err end transport.lastTransmit=now()trace(\"discovery\",out.type..\"_sent\",{to=target})return true end local function phraseTag(phrase,sessionId,hostId)return password.hmacSha256(phrase,\"ceetos/discovery/v2|\"..sessionId..\"|\"..tostring(hostId))end local function joinProof(phrase,sessionId,hostId,joinerId,nonce)return password.hmacSha256(phrase,\"ceetos/pair/join/v2|\"..sessionId..\"|\"..tostring(hostId)..\"|\"..tostring(joinerId)..\"|\"..nonce)end local function confirmProof(phrase,sessionId,hostId,joinerId,nonce,mode,master)return password.hmacSha256(phrase,\"ceetos/pair/confirm/v2|\"..sessionId..\"|\"..tostring(hostId)..\"|\"..tostring(joinerId)..\"|\"..nonce..\"|\"..tostring(mode)..\"|\"..tostring(master or\"\"))end local function linkKey(phrase,sessionId,hostId,joinerId,nonce)return password.derive(phrase,\"ceetos/link/v2|\"..sessionId..\"|\"..tostring(hostId)..\"|\"..tostring(joinerId)..\"|\"..nonce,64)end local function keyId(key)local value=password.hmacSha256(key,\"ceetos/key-id/v2\")return value and value:sub(1,16)or\"\"end local function activeSession()return discovery and(discovery.state==\"hosting\"or discovery.state==\"searching\"or discovery.state==\"candidate\"or discovery.state==\"confirming\")end local function setLast(state,message)lastDiscovery={state=state,message=message,at=now()}end local function persistPair(peerId,key,metadata,masterId)local value=config()local previous=value.peers[tostring(peerId)]value.peers[tostring(peerId)]={protocol=2,rekeyRequired=false,key=key,keyId=keyId(key),role=\"operator\",mode=metadata.mode or\"peer\",master=tostring(masterId or\"\")==id(),masterId=tostring(masterId or\"\"),label=metadata.label,}local saved,saveErr=pcall(save,value)if not saved then value.peers[tostring(peerId)]=previous configCache=value return false,tostring(saveErr or\"could not save network configuration\")end local health=healthFor(peerId)health.state,health.misses,health.awaiting,health.probeToken=\"healthy\",0,false,nil health.lastAuthenticated,health.stableSince=now(),now()remember(peerId,metadata.label,{mode=metadata.mode,masterId=masterId})routes[tostring(peerId)]=nil meshEnrollmentSends[tostring(peerId)]=nil meshDirty,routesDirty=true,true audit.log(\"local\",\"peer.rekeyed\",{peer=peerId,mode=metadata.mode})return true end local function markRejected(reason)if discovery then discovery=nil end setLast(\"cancelled\",reason)end local function discoveryAnnouncement()if not discovery or discovery.state~=\"hosting\"then return false,\"no active host discovery\"end return broadcast({type=\"discover\",session=discovery.id,tag=discovery.tag,label=discovery.label,mode=discovery.mode,master=discovery.masterSide,expires=discovery.expires,})end local function queueConfirmation(peerId,host)confirmations[host.id]=host local sent,err=handshake(peerId,{type=\"pair_confirm\",session=host.id,nonce=host.joinNonce,proof=host.confirmProof,label=host.label,mode=host.mode,master=host.masterId,})host.lastSent,host.retries=now(),0 if not sent then host.error=err end return sent,err end local function rollbackPendingConfirmation(packet)local sessionId,from=tostring(packet.session or\"\"),tostring(packet.from or\"\")local record=confirmations[sessionId]if not record or tostring(record.peer)~=from or type(record.keyId)~=\"string\"or type(record.key)~=\"string\"then return false end local expected=password.hmacSha256(record.key,\"ceetos/pair/reject/v2|\"..sessionId..\"|\"..from..\"|\"..id()..\"|persistence_failed\")if type(packet.proof)~=\"string\"or not password.constantTimeEqual(expected,packet.proof)then trace(\"discovery\",\"pair_rollback_rejected\",{from=from,reason=\"invalid_reject_proof\"})return false end local value,peer=config(),activePeer(config(),from)if not peer or peer.keyId~=record.keyId then return false end local previous=value.peers[from]value.peers[from]=nil local saved,saveErr=pcall(save,value)if not saved then value.peers[from],configCache=previous,value trace(\"discovery\",\"pair_rollback_failed\",{from=from,error=saveErr})return false end confirmations[sessionId],routes[from],meshEnrollmentSends[from]=nil,nil,nil setLast(\"cancelled\",\"pairing rejected: persistence failed on remote peer\")trace(\"discovery\",\"pair_rolled_back\",{from=from,reason=\"persistence_failed\"})return true end local function expireState(timestamp)if discovery and discovery.expires<=timestamp then local old=discovery.state discovery=nil setLast(\"expired\",old==\"searching\"and\"no matching discovery announcement received\"or\"discovery session expired\")trace(\"discovery\",\"session_expired\",{previous=old})end end local function boundMap(value,limit,timestamp)local entries={}for key,row in pairs(value)do entries[#entries+1]={key=key,at=type(row)==\"number\"and row or(type(row)==\"table\"and(row.expires or row.time or row.seen or row.lastAuthenticated or 0)or 0)}end table.sort(entries,function(a,b)if a.at~=b.at then return a.at<b.at end return tostring(a.key)<tostring(b.key)end)while#entries>limit do value[table.remove(entries,1).key]=nil end end local function pruneRouteBuckets(timestamp)local value=config()local entries={}for peerId,bucket in pairs(routes)do local latest=tonumber(bucket.changedAt)or 0 for _,candidate in ipairs(type(bucket.candidates)==\"table\"and bucket.candidates or{})do latest=math.max(latest,tonumber(candidate.expires)or 0)end entries[#entries+1]={peer=peerId,latest=latest}end table.sort(entries,function(a,b)if a.latest~=b.latest then return a.latest<b.latest end return tostring(a.peer)<tostring(b.peer)end)while#entries>ROUTE_BUCKET_LIMIT do local old=table.remove(entries,1)routes[old.peer],routesDirty=nil,true trace(\"routing\",\"route_bucket_evicted\",{destination=old.peer})end for peerId,state in pairs(linkHealth)do if not activeLink(value,peerId)and not routes[peerId]then linkHealth[peerId]=nil end end for peerId in pairs(advertisedRoutes)do if not activeLink(value,peerId)then advertisedRoutes[peerId]=nil end end boundMap(meshOffers,MESH_STATE_LIMIT,timestamp)boundMap(meshSeen,MESH_STATE_LIMIT,timestamp)end local function controller(value)value=value or config()if value.controller and tostring(value.controller)~=\"\"then local chosen=tostring(value.controller)if chosen==id()or activeLink(value,chosen)then return chosen end local known=routes[chosen]if known then if known.expires and known.expires>now()then return chosen end if type(known.candidates)==\"table\"then for _,candidate in ipairs(known.candidates)do if(candidate.expires or 0)>now()and activeLink(value,candidate.next)then return chosen end end end end trace(\"routing\",\"controller_unreachable\",{controller=chosen})end local selected=tonumber(id())or 0 for peerId in pairs(value.peers)do if activePeer(value,peerId)then local n=tonumber(peerId);if n and n<selected then selected=n end end end for peerId,route in pairs(routes)do local reachable=route.expires and route.expires>now()if type(route.candidates)==\"table\"then reachable=false for _,candidate in ipairs(route.candidates)do if(candidate.expires or 0)>now()then reachable=true;break end end end if reachable then local n=tonumber(peerId);if n and n<selected then selected=n end end end return tostring(selected)end local function meshConfig(value)value=value or config()local mesh=value.mesh if type(mesh)==\"table\"and type(mesh.id)==\"string\"and#mesh.id>=16 and type(mesh.root)==\"string\"and#mesh.root>=32 and tonumber(mesh.epoch)then return mesh end return nil end local function meshAuthority(mesh,fallback)local candidate=type(mesh)==\"table\"and mesh.controller or fallback if type(candidate)~=\"string\"and type(candidate)~=\"number\"then return nil end candidate=tostring(candidate)if#candidate==0 or#candidate>32 or not candidate:match(\"^%-?%d+$\")then return nil end return candidate end local function compareMeshAuthority(left,right)local leftNumber,rightNumber=tonumber(left),tonumber(right)if leftNumber and rightNumber and leftNumber~=rightNumber then return leftNumber<rightNumber and-1 or 1 end left,right=tostring(left or\"\"),tostring(right or\"\")if left==right then return 0 end return left<right and-1 or 1 end local function acceptPreferredMesh(existing,incoming,fallbackAuthority)if not existing then return true,\"first mesh enrollment\"end if existing.id==incoming.id then if tonumber(incoming.epoch)>=tonumber(existing.epoch)then return true,\"same mesh refresh\"end return false,\"existing mesh has a newer epoch\"end local existingAuthority=meshAuthority(existing,fallbackAuthority)local incomingAuthority=meshAuthority(incoming,fallbackAuthority)if not incomingAuthority then return false,\"incoming mesh has no valid controller\"end if not existingAuthority then return true,\"legacy mesh has no controller\"end local authorityOrder=compareMeshAuthority(incomingAuthority,existingAuthority)if authorityOrder<0 then return true,\"lower controller mesh wins\"end if authorityOrder>0 then return false,\"existing lower controller mesh wins\"end if tonumber(incoming.epoch)~=tonumber(existing.epoch)then return tonumber(incoming.epoch)>tonumber(existing.epoch),\"same controller epoch comparison\"end return tostring(incoming.id)<tostring(existing.id),\"same controller mesh ID comparison\"end local function meshMaterial(packet)return canonical({protocol=packet.protocol,type=packet.type,from=packet.from,to=packet.to,mesh=packet.mesh,epoch=packet.epoch,nonce=packet.nonce,offer=packet.offer,timestamp=packet.timestamp})end local function meshTag(mesh,packet)local material=meshMaterial(packet)return material and password.hmacSha256(mesh.root,material)or nil end local function meshPacket(typeName,target,fields)local value,mesh=config(),meshConfig()if not mesh then return false,\"mesh enrollment is pending\"end local packet=copy(fields or{})packet.protocol,packet.type,packet.from,packet.to=PROTOCOL,typeName,id(),target and tostring(target)or 0 packet.mesh,packet.epoch,packet.timestamp=mesh.id,mesh.epoch,now()packet.tag=meshTag(mesh,packet)if not packet.tag then return false,\"could not authenticate mesh packet\"end openChannels(value)local links=wirelessModems()if#links==0 then return false,\"no wireless modem attached\"end local sent,err=transmit(links,value.channel or CHANNEL,packet)if sent then trace(\"routing\",typeName..\"_sent\",{to=packet.to,mesh=mesh.id:sub(1,8)})end return sent,err end local function deriveAutoEdge(mesh,peerId,firstNonce,secondNonce)local left,right=tostring(id()),tostring(peerId)if left>right then left,right=right,left end return password.hmacSha256(mesh.root,\"ceetos/mesh-edge/v1|\"..tostring(mesh.epoch)..\"|\"..left..\"|\"..right..\"|\"..tostring(firstNonce)..\"|\"..tostring(secondNonce))end local function ownsMeshOffer(peerId)local ours,theirs=tonumber(id()),tonumber(peerId)if ours and theirs then return ours<theirs end return tostring(id())<tostring(peerId)end local function installAutoEdge(mesh,peerId,firstNonce,secondNonce,peerLabel)peerId=tostring(peerId)local value=config()if activePeer(value,peerId)then return false,\"manual direct link already exists\"end local key=deriveAutoEdge(mesh,peerId,firstNonce,secondNonce)if not key then return false,\"could not derive mesh edge\"end autoEdges[peerId]={key=key,label=peerLabel,mesh=mesh.id,epoch=mesh.epoch,expires=now()+ROUTE_TTL,firstNonce=firstNonce,secondNonce=secondNonce}local state=healthFor(peerId)state.state,state.successes,state.misses,state.lastAuthenticated,state.stableSince=\"healthy\",2,0,now(),now()routesDirty,meshDirty=true,true trace(\"routing\",\"mesh_edge_active\",{peer=peerId,mesh=mesh.id:sub(1,8)})return true end local function meshEnrollmentAad(from,to,meshId,epoch,nonceCounter)return canonical({protocol=PROTOCOL,type=\"mesh_enroll\",from=tostring(from),to=tostring(to),mesh=meshId,epoch=tonumber(epoch),counter=tonumber(nonceCounter)})end local function sendEnrollment(peerId)local value,mesh,peer=config(),meshConfig(),activePeer(config(),peerId)if not mesh or not peer then return false,\"mesh or direct link unavailable\"end local authority=meshAuthority(mesh,controller(value))if not authority then return false,\"mesh controller is invalid\"end local _,nonceCounter=counter(peerId)local aad=meshEnrollmentAad(id(),peerId,mesh.id,mesh.epoch,nonceCounter)local nonce=meshCrypto.nonce(peer.key,nonceCounter,\"mesh-enroll|\"..id()..\"|\"..tostring(peerId))local plaintext=textutils.serialise({id=mesh.id,epoch=mesh.epoch,root=mesh.root,controller=authority},{compact=true})local envelope,err=meshCrypto.seal(peer.key,nonce,aad,plaintext)if not envelope then return false,err end local sent,sendErr=send(peerId,{type=\"mesh_enroll\",body={mesh=mesh.id,epoch=mesh.epoch,counter=nonceCounter,envelope=envelope},request_id=newRequestId()},peerId)if sent then trace(\"routing\",\"mesh_enrollment_sent\",{to=peerId,mesh=mesh.id:sub(1,8),controller=authority,epoch=mesh.epoch})else trace(\"routing\",\"mesh_enrollment_failed\",{to=peerId,error=sendErr})end return sent,sendErr end local function acceptEnrollment(from,peer,packet)if not activePeer(config(),from)then return rejectPacket(\"mesh enrollment requires a manual direct link\",packet)end local body=packet.body if type(body)~=\"table\"or type(body.mesh)~=\"string\"or type(body.envelope)~=\"table\"or not tonumber(body.epoch)or not tonumber(body.counter)then return rejectPacket(\"malformed_mesh_enrollment\",packet)end local aad=meshEnrollmentAad(from,id(),body.mesh,body.epoch,body.counter)local plaintext,err=meshCrypto.open(peer.key,body.envelope,aad)if not plaintext then return rejectPacket(err or\"mesh enrollment authentication failed\",packet)end local ok,mesh=pcall(textutils.unserialise,plaintext)if not ok or type(mesh)~=\"table\"or mesh.id~=body.mesh or tonumber(mesh.epoch)~=tonumber(body.epoch)or type(mesh.root)~=\"string\"or#mesh.root<32 or not meshAuthority(mesh)then return rejectPacket(\"invalid_mesh_enrollment\",packet)end local value,existing=config(),meshConfig()if existing and existing.id==mesh.id then if existing.root~=mesh.root then return rejectPacket(\"conflicting_mesh_root\",packet)end local currentAuthority=meshAuthority(existing,controller(value))local incomingAuthority=meshAuthority(mesh,controller(value))if currentAuthority and incomingAuthority and compareMeshAuthority(currentAuthority,incomingAuthority)<0 then mesh.controller=currentAuthority elseif currentAuthority then mesh.controller=incomingAuthority or currentAuthority end mesh.epoch=math.max(tonumber(existing.epoch)or 0,tonumber(mesh.epoch)or 0)end local accepted,reason=acceptPreferredMesh(existing,mesh,controller(value))if not accepted then trace(\"routing\",\"mesh_enrollment_kept\",{from=from,reason=reason,mesh=existing and existing.id:sub(1,8)});return end local changedRoot=not existing or existing.id~=mesh.id or tonumber(existing.epoch)~=tonumber(mesh.epoch)or tostring(meshAuthority(existing,controller(value))or\"\")~=tostring(meshAuthority(mesh,controller(value))or\"\")value.mesh={id=mesh.id,epoch=tonumber(mesh.epoch),root=mesh.root,controller=meshAuthority(mesh),enrolledAt=now(),enrolledBy=tostring(from)}save(value)if changedRoot then meshEnrollmentSends={}end meshDirty,routesDirty=true,true trace(\"routing\",\"mesh_enrolled\",{from=from,mesh=mesh.id:sub(1,8),controller=mesh.controller,epoch=mesh.epoch,reason=reason})end local function ensureMeshEnrollment(timestamp)local value,mesh=config(),meshConfig()if not mesh then if(M._meshBootstrapAt or 0)>timestamp or controller(value)~=id()then return end value.mesh={id=password.newSalt(\"mesh-id:\"..id()),root=password.newSalt(\"mesh-root:\"..id()),epoch=1,createdAt=timestamp,controller=id()}save(value);mesh,meshDirty,routesDirty=value.mesh,true,true trace(\"routing\",\"mesh_created\",{mesh=mesh.id:sub(1,8),epoch=mesh.epoch})elseif not meshAuthority(mesh)then mesh.controller=controller(value)value.mesh,mesh=mesh,mesh save(value)trace(\"routing\",\"mesh_controller_migrated\",{mesh=mesh.id:sub(1,8),controller=mesh.controller})end for _,peerId in ipairs(allDirectLinks(value))do if activePeer(value,peerId)then local previous=meshEnrollmentSends[peerId]if not previous or previous.mesh~=mesh.id or previous.epoch~=mesh.epoch or timestamp-previous.at>=MESH_FULL_INTERVAL then local sent=sendEnrollment(peerId)if sent then meshEnrollmentSends[peerId]={mesh=mesh.id,epoch=mesh.epoch,at=timestamp}end end end end end local function meshBeacon(timestamp)local mesh=meshConfig()if not mesh or timestamp-lastMeshBeacon<MESH_BEACON_INTERVAL then return end lastMeshBeacon=timestamp meshPacket(\"mesh_beacon\",nil,{nonce=password.newSalt(\"mesh-beacon:\"..id()),label=label()})end local function meshEdgeRefreshReason(mesh,peerId,timestamp)local edge=autoEdges[peerId]if not edge then return\"missing\"end if edge.mesh~=mesh.id or tonumber(edge.epoch)~=tonumber(mesh.epoch)then return\"mesh_epoch_changed\"end local health=linkHealth[peerId]if not health then return\"unobserved\"end if health.state~=\"healthy\"then return\"link_\"..tostring(health.state)end if timestamp-(tonumber(health.lastAuthenticated)or 0)>=MESH_BEACON_INTERVAL then return\"authentication_stale\"end return nil end local function offerMeshEdge(mesh,peerId,reason)local pending=meshOffers[peerId]if pending and pending.expires>now()then return false,\"mesh edge offer already pending\"end local nonce=password.newSalt(\"mesh-offer:\"..peerId)local sent,err=meshPacket(\"mesh_edge_offer\",peerId,{nonce=nonce,label=label()})if not sent then trace(\"routing\",\"mesh_edge_offer_failed\",{peer=peerId,reason=reason,error=err})return false,err end meshOffers[peerId]={offer=nonce,expires=now()+DISCOVERY_TTL}trace(\"routing\",\"mesh_edge_offer_sent\",{peer=peerId,reason=reason})return true end local function verifyMeshPacket(packet)local mesh=meshConfig()if not mesh or type(packet)~=\"table\"or packet.protocol~=PROTOCOL or packet.mesh~=mesh.id or tonumber(packet.epoch)~=tonumber(mesh.epoch)then return false,\"mesh membership proof failed\"end if type(packet.from)~=\"string\"and type(packet.from)~=\"number\"then return false,\"invalid mesh sender\"end if type(packet.nonce)~=\"string\"or#packet.nonce<16 or#packet.nonce>128 then return false,\"invalid mesh nonce\"end if math.abs(now()-(tonumber(packet.timestamp)or 0))>ROUTE_TTL then return false,\"stale mesh packet\"end local material,tag=meshMaterial(packet),packet.tag local expected=material and password.hmacSha256(mesh.root,material)if type(tag)~=\"string\"or not expected or not password.constantTimeEqual(expected,tag)then return false,\"invalid mesh packet tag\"end local replay=tostring(packet.from)..\"|\"..tostring(packet.type)..\"|\"..tostring(packet.nonce)..\"|\"..tostring(packet.offer or\"\")if meshSeen[replay]and meshSeen[replay]+ROUTE_TTL>now()then return false,\"replayed mesh packet\"end meshSeen[replay]=now()return true,mesh end local function handleMeshPacket(packet,distance)local valid,meshOrErr=verifyMeshPacket(packet)if not valid then return rejectPacket(meshOrErr,packet,nil,distance)end local mesh,from=meshOrErr,tostring(packet.from)if from==id()then return end if packet.type==\"mesh_beacon\"then remember(from,packet.label,{mesh=mesh.id})trace(\"routing\",\"mesh_beacon_received\",{peer=from,mesh=mesh.id:sub(1,8),distance=distance})if not activePeer(config(),from)and ownsMeshOffer(from)then local reason=meshEdgeRefreshReason(mesh,from,now())if reason then offerMeshEdge(mesh,from,reason)end end return end if not isForUs(packet.to)then return rejectPacket(\"mesh packet addressed to another peer\",packet,nil,distance)end if packet.type==\"mesh_edge_offer\"then if activePeer(config(),from)then return end trace(\"routing\",\"mesh_edge_offer_received\",{peer=from,mesh=mesh.id:sub(1,8),distance=distance})local response=password.newSalt(\"mesh-accept:\"..from)local installed,err=installAutoEdge(mesh,from,packet.nonce,response,packet.label)if not installed then return rejectPacket(err or\"could not install mesh edge\",packet,nil,distance)end local sent,sendErr=meshPacket(\"mesh_edge_accept\",from,{nonce=response,offer=packet.nonce,label=label()})if sent then trace(\"routing\",\"mesh_edge_accept_sent\",{peer=from})end return sent,sendErr end if packet.type==\"mesh_edge_accept\"then local offer=meshOffers[from]if not offer or offer.expires<=now()or offer.offer~=packet.offer then return rejectPacket(\"unexpected mesh edge acceptance\",packet,nil,distance)end meshOffers[from]=nil local installed,err=installAutoEdge(mesh,from,offer.offer,packet.nonce,packet.label)if installed then trace(\"routing\",\"mesh_edge_accept_received\",{peer=from})end return installed,err end end local function probeLinks(timestamp)local value=config()for _,peerId in ipairs(allDirectLinks(value))do local health=healthFor(peerId)local interval=health.state==\"offline\"and PROBE_INTERVAL*3 or PROBE_INTERVAL if timestamp-(health.lastAuthenticated or 0)>=interval and timestamp-(health.lastProbe or 0)>=interval then if health.awaiting then health.misses=(health.misses or 0)+1 health.loss=math.min(1,((health.loss or 0)*3+1)/4)local nextState=health.misses>=PROBE_OFFLINE and\"offline\"or(health.misses>=PROBE_DEGRADED and\"degraded\"or health.state)if nextState~=health.state then health.state,health.successes,health.recoveredAt=nextState,0,nil trace(\"routing\",\"link_\"..nextState,{peer=peerId,misses=health.misses})end routesDirty=true end health.awaiting,health.lastProbe,health.probeToken=true,timestamp,password.newSalt(\"mesh-probe:\"..peerId)send(peerId,{type=\"mesh_probe\",body={token=health.probeToken},request_id=newRequestId()},peerId)end end end local function advertisedMembers(value,peerId,timestamp)local members={}for other,manual in pairs(value.peers)do if other~=peerId and activeLink(value,other)and linkState(other)==\"healthy\"then members[other]={label=manual.label,mode=manual.mode,masterId=manual.masterId,hops=1,kind=\"direct\"}end end for other,edge in pairs(autoEdges)do if other~=peerId and edge.expires>timestamp and linkState(other)==\"healthy\"then members[other]={label=edge.label,mode=\"peer\",hops=1,kind=\"mesh\"}end end for other in pairs(routes)do if other~=peerId and not members[other]then local candidate=primaryCandidate(other,value,false)if candidate and candidate.next~=peerId and linkState(candidate.next)==\"healthy\"then members[other]={label=candidate.label,mode=candidate.mode,masterId=candidate.masterId,hops=candidate.hops,kind=candidate.kind or\"route\"}end end end return members end local function sameAdvertisement(left,right)local leftValue,leftErr=canonical(left)local rightValue,rightErr=canonical(right)return leftValue~=nil and rightValue~=nil and not leftErr and not rightErr and leftValue==rightValue end local function advertiseRoutes(force)local timestamp,value=now(),config()if not force and not routesDirty and timestamp-lastRouteAdvertisement<ROUTE_INTERVAL then return end lastRouteAdvertisement,routesDirty=timestamp,false local full=timestamp-lastMeshFull>=MESH_FULL_INTERVAL if full then lastMeshFull=timestamp end for _,peerId in ipairs(allDirectLinks(value))do if linkUsable(value,peerId,false)then local desired,previous=advertisedMembers(value,peerId,timestamp),advertisedRoutes[peerId]or{}local members,withdrawals={},{}if full then members=desired else for target,meta in pairs(desired)do if not sameAdvertisement(meta,previous[target])then members[target]=meta end end for target in pairs(previous)do if not desired[target]then withdrawals[#withdrawals+1]=target end end end table.sort(withdrawals,function(a,b)return tostring(a)<tostring(b)end)if full or next(members)~=nil or#withdrawals>0 then routeSequence=routeSequence+1 local body={sequence=routeSequence,routeVersion=2,session=routeSession,full=full,controller=controller(value),members=members,withdrawals=withdrawals,}if send(peerId,{type=\"route_advertise\",body=body,routeSafe=true,request_id=newRequestId()})then advertisedRoutes[peerId]=copy(desired)trace(\"routing\",full and\"route_snapshot_sent\"or\"route_delta_sent\",{to=peerId,changes=(full and 0 or#withdrawals)})end end else advertisedRoutes[peerId]=nil end end end function M.start()migrateV1Links()loadSecurity()assert(meshCrypto.selfTest(),\"CeetOS mesh cryptography self-test failed\")openChannels(config())local status=M.transportStatus()trace(\"transport\",status.wireless and\"wireless_modem_detected\"or\"wireless_modem_missing\",{name=status.name})routesDirty,M._meshBootstrapAt=true,now()end function M.transportStatus(value)local wireless=wirelessModems()transport.modem,transport.wireless=#modems()>0,#wireless>0 transport.name=wireless[1]and wireless[1].name or nil local out=copy(transport)out.wirelessLinks,out.wirelessLinkCount={},#wireless for _,item in ipairs(wireless)do out.wirelessLinks[#out.wirelessLinks+1]=item.name end out.usesAllWirelessLinks,out.protocol=true,2 out.directPeers,out.manualDirectPeers,out.meshPeers,out.rekeyRequiredPeers,out.routedPeers=0,0,0,0,0 out.healthyLinks,out.degradedLinks,out.offlineLinks=0,0,0 value=value or config()for peerId,peer in pairs(value.peers)do if activePeer(value,peerId)then out.directPeers,out.manualDirectPeers=out.directPeers+1,out.manualDirectPeers+1 elseif peer.rekeyRequired then out.rekeyRequiredPeers=out.rekeyRequiredPeers+1 end end for peerId,edge in pairs(autoEdges)do if edge.expires>now()then out.meshPeers,out.directPeers=out.meshPeers+1,out.directPeers+1 end end for peerId,health in pairs(linkHealth)do if health.state==\"healthy\"then out.healthyLinks=out.healthyLinks+1 elseif health.state==\"degraded\"then out.degradedLinks=out.degradedLinks+1 elseif health.state==\"offline\"then out.offlineLinks=out.offlineLinks+1 end end for peerId,bucket in pairs(routes)do local usable=primaryCandidate(peerId,value,false)if usable and not value.peers[peerId]and not autoEdges[peerId]then out.routedPeers=out.routedPeers+1 end end local mesh=meshConfig(value)out.mesh=mesh and{id=mesh.id:sub(1,8),epoch=mesh.epoch,controller=meshAuthority(mesh,controller(value)),enrolled=true}or{enrolled=false}out.health={healthy=out.healthyLinks,degraded=out.degradedLinks,offline=out.offlineLinks}return out end function M.transportTrace(kind)if kind==\"discovery\"or kind==\"routing\"or kind==\"transport\"then return copy(traces[kind])end return{discovery=copy(traces.discovery),routing=copy(traces.routing),transport=copy(traces.transport)}end function M.discovery()expireState(now())return copy(discovery)end function M.discoveryStatus(transportStatus)expireState(now())local candidate=discovery and discovery.candidate and{id=discovery.candidate.from,label=discovery.candidate.label,mode=discovery.candidate.mode,master=discovery.candidate.master,}or nil local session=discovery and{session=discovery.id,state=discovery.state,mode=discovery.mode,master=discovery.masterSide,control=discovery.control,expires=discovery.expires,retries=discovery.retries or 0,candidate=candidate and copy(candidate)or nil,}or nil local state=session and session.state or(lastDiscovery and lastDiscovery.state or\"inactive\")return{state=state,active=session~=nil,lastState=lastDiscovery and lastDiscovery.state or\"inactive\",lastMessage=lastDiscovery and lastDiscovery.message or nil,session=session,searching=session and session.state==\"searching\"or false,candidate=candidate,discovered=#M.seenPeers(),transport=transportStatus and copy(transportStatus)or M.transportStatus(),message=(discovery and discovery.error)or(lastDiscovery and lastDiscovery.message),}end function M.startDiscovery(mode,masterSide,phrase)if activeSession()then return false,\"cancel the active discovery session first\"end local normalized,phraseErr=M.normalizePhrase(phrase)if not normalized then return false,phraseErr end mode=mode==\"master\"and\"master\"or\"peer\"masterSide=masterSide==\"joiner\"and\"joiner\"or\"initiator\"local sessionId=password.newSalt(\"discovery:\"..id())discovery={id=sessionId,state=\"hosting\",phrase=normalized,tag=phraseTag(normalized,sessionId,id()),mode=mode,masterSide=masterSide,label=label(),initiator=id(),expires=now()+DISCOVERY_TTL,control=password.newSalt(\"discovery-control:\"..id()),retries=0,lastBroadcast=0,}local sent,err=discoveryAnnouncement()if not sent then discovery=nil;setLast(\"cancelled\",err);return false,err end discovery.lastBroadcast=now()setLast(\"hosting\")return true,{state=\"hosting\",session=sessionId,expires=discovery.expires}end function M.joinDiscovery(phrase)if activeSession()then return false,\"cancel the active discovery session first\"end local normalized,phraseErr=M.normalizePhrase(phrase)if not normalized then return false,phraseErr end local candidate for _,item in pairs(discovered)do if item.expires>now()and password.constantTimeEqual(item.tag or\"\",phraseTag(normalized,item.session,item.from))then candidate=item;break end end discovery={state=\"searching\",phrase=normalized,expires=now()+DISCOVERY_TTL,control=password.newSalt(\"discovery-control:\"..id()),retries=0,lastQuery=0,}if candidate then discovery.state,discovery.candidate=\"candidate\",candidate return true,{state=\"candidate\",candidate={id=candidate.from,label=candidate.label,mode=candidate.mode,master=candidate.master}}end local sent,err=broadcast({type=\"discover_query\"})if not sent then discovery=nil;setLast(\"cancelled\",err);return false,err end discovery.lastQuery=now()setLast(\"searching\",\"waiting for matching discovery announcement\")return true,{state=\"searching\"}end function M.confirmDiscovery()if not discovery or discovery.state~=\"candidate\"or not discovery.candidate then return false,\"no discovery candidate is awaiting confirmation\"end local candidate,value=discovery.candidate,config()local existing=value.peers[tostring(candidate.from)]if existing and not existing.rekeyRequired then return false,\"computer is already a network member\"end if routes[tostring(candidate.from)]and not existing then return false,\"computer is already a routed network member\"end local nonce=password.newSalt(\"pair-join:\"..candidate.session)local proof=joinProof(discovery.phrase,candidate.session,candidate.from,id(),nonce)discovery.state,discovery.joinNonce,discovery.joinProof,discovery.lastSent,discovery.retries=\"confirming\",nonce,proof,now(),0 local sent,err=handshake(candidate.from,{type=\"pair_join\",session=candidate.session,nonce=nonce,proof=proof,label=label()})if not sent then discovery.error=err;return false,err end trace(\"discovery\",\"pair_confirmation_requested\",{peer=candidate.from})return true,{state=\"confirming\",peer=tostring(candidate.from),label=candidate.label,expires=discovery.expires}end function M.cancelDiscovery(expectedControl,source)if not discovery then return false,\"no active discovery session\"end if type(expectedControl)~=\"string\"or expectedControl==\"\"then return false,\"discovery cancellation requires the active session control token\"end if not password.constantTimeEqual(expectedControl,discovery.control or\"\")then trace(\"discovery\",\"stale_cancel_rejected\",{source=tostring(source or\"unknown\"):sub(1,32)})return false,\"discovery session changed; refresh before cancelling\"end if discovery and discovery.state==\"hosting\"then broadcast({type=\"discover_cancel\",session=discovery.id})end discovery=nil setLast(\"cancelled\",\"discovery cancelled\")trace(\"discovery\",\"session_cancelled\",{source=tostring(source or\"unknown\"):sub(1,32)})return true end function M.normalizePhrase(value)if type(value)~=\"string\"then return nil,\"phrase is required\"end if value:find(\"[%z\\1-\\31\\127]\")then return nil,\"phrase contains control characters\"end value=value:gsub(\"^%s+\",\"\"):gsub(\"%s+$\",\"\")if#value<8 then return nil,\"phrase must be at least 8 characters\"end if#value>128 then return nil,\"phrase must be at most 128 characters\"end return value end function M.tick()local timestamp=now()expireState(timestamp)if discovery then if discovery.state==\"hosting\"and timestamp-(discovery.lastBroadcast or 0)>=math.min(30000,DISCOVERY_INTERVAL*(2^math.min(discovery.retries or 0,4)))then local sent,err=discoveryAnnouncement()discovery.lastBroadcast=timestamp discovery.retries=(discovery.retries or 0)+1 if not sent then discovery.error=err end elseif discovery.state==\"searching\"and timestamp-(discovery.lastQuery or 0)>=DISCOVERY_INTERVAL then local sent,err=broadcast({type=\"discover_query\"})discovery.lastQuery,discovery.retries=timestamp,(discovery.retries or 0)+1 if not sent then discovery.error=err end elseif discovery.state==\"confirming\"and timestamp-(discovery.lastSent or 0)>=PAIR_RETRY then local candidate=discovery.candidate local sent,err=handshake(candidate.from,{type=\"pair_join\",session=candidate.session,nonce=discovery.joinNonce,proof=discovery.joinProof,label=label()})discovery.lastSent,discovery.retries=timestamp,(discovery.retries or 0)+1 if not sent then discovery.error=err end end end for sessionId,item in pairs(discovered)do if item.expires<=timestamp then discovered[sessionId]=nil end end for sessionId,record in pairs(confirmations)do if record.expires<=timestamp then confirmations[sessionId]=nil elseif timestamp-(record.lastSent or 0)>=PAIR_RETRY then queueConfirmation(record.peer,record)end end for peerId,bucket in pairs(routes)do local kept={}for _,candidate in ipairs(routeBucket(peerId).candidates)do if candidate.expires>timestamp then kept[#kept+1]=candidate end end bucket.candidates=kept if#kept==0 and not activeLink(config(),peerId)then routes[peerId],routesDirty=nil,true end end for peerId,edge in pairs(autoEdges)do if edge.expires<=timestamp then autoEdges[peerId],routesDirty=nil,true;trace(\"routing\",\"mesh_edge_expired\",{peer=peerId})end end for key,seenAt in pairs(meshSeen)do if seenAt+ROUTE_TTL<=timestamp then meshSeen[key]=nil end end for peerId,offer in pairs(meshOffers)do if offer.expires<=timestamp then meshOffers[peerId]=nil end end for requestId,seenAt in pairs(seenPackets)do if seenAt+ROUTE_TTL<=timestamp then seenPackets[requestId]=nil end end for requestId,route in pairs(reverseRoutes)do if route.expires<=timestamp then reverseRoutes[requestId]=nil end end boundMap(meshSeen,MESH_STATE_LIMIT,timestamp)boundMap(meshOffers,MESH_STATE_LIMIT,timestamp)pruneRouteBuckets(timestamp)ensureMeshEnrollment(timestamp)meshBeacon(timestamp)probeLinks(timestamp)advertiseRoutes(false)end function M.peers(value,seen)local out={}value,seen=value or config(),seen or read(SEEN_PATH,{})for peerId,peer in pairs(value.peers)do out[#out+1]={id=peerId,label=peer.label or(seen[peerId]and seen[peerId].label),role=peer.role or\"operator\",mode=peer.mode or\"peer\",master=peer.master==true,masterId=peer.masterId,direct=true,active=activePeer(value,peerId)~=nil,rekeyRequired=peer.rekeyRequired==true or peer.protocol~=2,health=linkState(peerId),auto=false,}end for peerId,edge in pairs(autoEdges)do if not value.peers[peerId]and edge.expires>now()then out[#out+1]={id=peerId,label=edge.label or(seen[peerId]and seen[peerId].label),role=\"mesh\",mode=\"peer\",direct=true,auto=true,active=true,health=linkState(peerId)}end end for peerId in pairs(routes)do if not value.peers[peerId]and not autoEdges[peerId]then local route=primaryCandidate(peerId,value,false)if route then out[#out+1]={id=peerId,label=route.label or(seen[peerId]and seen[peerId].label),role=\"routed\",mode=route.mode or\"peer\",masterId=route.masterId,routed=true,hops=route.hops,next=route.next,health=linkState(route.next),alternates=#routeBucket(peerId).candidates-1}end end end table.sort(out,function(a,b)return tonumber(a.id)<tonumber(b.id)end)return out end function M.directPeers()local out={}for _,peer in ipairs(M.peers())do if peer.direct and peer.active then out[#out+1]=peer end end return out end function M.seenPeers()local out,timestamp={},now()for peerId,item in pairs(read(SEEN_PATH,{}))do if type(item)==\"table\"and tonumber(item.seen)and tonumber(item.seen)+SEEN_PEER_TTL>timestamp then out[#out+1]={id=peerId,label=item.label,seen=item.seen,session=item.session}end end table.sort(out,function(a,b)return tonumber(a.seen)>tonumber(b.seen)end)while#out>SEEN_PEER_LIMIT do table.remove(out)end table.sort(out,function(a,b)return tonumber(a.id)<tonumber(b.id)end)return out end function M.routeStatus(target,value)value,out=value or config(),{}local function row(peerId)local selected=primaryCandidate(peerId,value,false)if not selected then return end local health=healthFor(selected.next)local alternates={}for _,candidate in ipairs(validCandidates(peerId,value))do if tostring(candidate.next)~=tostring(selected.next)then local state=healthFor(candidate.next)alternates[#alternates+1]={next=candidate.next,hops=candidate.hops,kind=candidate.kind or\"route\",health=state.state,loss=state.loss or 0,rtt=state.rtt,distance=state.lastDistance}end end table.sort(alternates,compareCandidates)while#alternates>ROUTE_CANDIDATE_LIMIT-1 do table.remove(alternates)end out[#out+1]={id=tostring(peerId),label=selected.label,next=selected.next,hops=selected.hops,kind=selected.kind or\"route\",health=health.state,loss=health.loss or 0,rtt=health.rtt,distance=health.lastDistance,selectedAt=routeBucket(peerId).changedAt,reason=routeBucket(peerId).reason,alternates=alternates,}end if target then row(tostring(target))else local ids,seen={},{}for peerId in pairs(value.peers)do ids[#ids+1],seen[peerId]=peerId,true end for peerId in pairs(autoEdges)do if not seen[peerId]then ids[#ids+1],seen[peerId]=peerId,true end end for peerId in pairs(routes)do if not seen[peerId]then ids[#ids+1],seen[peerId]=peerId,true end end table.sort(ids,function(a,b)return tonumber(a)<tonumber(b)end)for _,peerId in ipairs(ids)do row(peerId)end end return out end function M.statusSnapshot(compact)local value,seen=config(),read(SEEN_PATH,{})local transport=M.transportStatus(value)local snapshot={protocol=2,transport=transport,discovery=M.discoveryStatus(transport),peers=M.peers(value,seen),routes=M.routeStatus(nil,value),controller=M.controller(),}if compact~=true then snapshot.trace=M.transportTrace()end return snapshot end function M.controller()return controller()end function M.isDirectController(from,packet)return tostring(from)==tostring(controller())and type(packet)==\"table\"and tostring(packet.origin or\"\")==tostring(from)end function M.setController(value)assert(tonumber(value),\"controller must be a computer ID\")local saved=config();saved.controller=tostring(value);save(saved);routesDirty=true audit.log(\"local\",\"jobs.controller\",{controller=saved.controller})return saved.controller end function M.request(target,typeName,body,options)if type(typeName)~=\"string\"or#typeName==0 or#typeName>64 then return false,\"invalid request type\"end local destination=tostring(target)local value=config()if not primaryCandidate(destination,value,options and options.safe==true)then return false,\"peer is unreachable, degraded, or requires rekey\"end return send(destination,{type=typeName,body=body or{},origin=id(),routeSafe=options and options.safe==true,balanceSafe=options and options.safe==true,confidential=options and options.confidential==true,request_id=newRequestId()})end function M.reply(request,typeName,body,options)if type(request)~=\"table\"or type(typeName)~=\"string\"or#typeName==0 or#typeName>64 then return false,\"invalid reply\"end local requestId,destination=request.request_id,tostring(request.origin or\"\")local reverse=type(requestId)==\"string\"and reverseRoutes[requestId]or nil if destination==\"\"or not reverse or reverse.expires<=now()or reverse.origin~=destination then return false,\"reverse reply route is unavailable\"end return send(destination,{type=typeName,body=body or{},origin=id(),destination=destination,reply_to=requestId,routeSafe=options and options.safe==true,confidential=options and options.confidential==true,request_id=newRequestId(),},reverse.previous)end function M.offer()return false,\"legacy pairing is disabled; use phrase discovery\"end function M.accept()return false,\"legacy pairing is disabled; use phrase discovery\"end function M.discover()return broadcast({type=\"discover_query\"})end rejectPacket=function(reason,packet,channel,distance)transport.rejected=transport.rejected+1 trace(\"transport\",\"packet_rejected\",{reason=reason,type=type(packet)==\"table\"and packet.type or nil,channel=channel,distance=distance,from=type(packet)==\"table\"and packet.from or nil})end local function handleDiscovery(packet,channel,distance)local from=tostring(packet.from)if packet.type==\"discover_cancel\"and isBroadcast(packet.to)then discovered[tostring(packet.session)]=nil trace(\"discovery\",\"cancel_received\",{from=from,distance=distance})return end if packet.type==\"discover_query\"and isBroadcast(packet.to)and discovery and discovery.state==\"hosting\"then discoveryAnnouncement()trace(\"discovery\",\"query_answered\",{from=from,distance=distance})return end if packet.type==\"discover\"and isBroadcast(packet.to)and from~=id()then if type(packet.session)~=\"string\"or#packet.session<16 or type(packet.tag)~=\"string\"or#packet.tag~=64 then return rejectPacket(\"malformed_discovery\",packet,channel,distance)end local timestamp=now()for sessionId,item in pairs(discovered)do if item.expires<=timestamp then discovered[sessionId]=nil end end if discovery and(discovery.state==\"searching\"or discovery.state==\"candidate\")and password.constantTimeEqual(packet.tag,phraseTag(discovery.phrase,packet.session,from))then local expires,candidates=tonumber(packet.expires)or(timestamp+DISCOVERY_TTL),0 for _ in pairs(discovered)do candidates=candidates+1 end if not discovered[packet.session]and candidates>=DISCOVERY_CANDIDATE_LIMIT then return rejectPacket(\"discovery candidate capacity reached\",packet,channel,distance)end discovered[packet.session]={session=packet.session,from=from,tag=packet.tag,label=tostring(packet.label or(\"Computer \"..from)):sub(1,128),mode=packet.mode==\"master\"and\"master\"or\"peer\",master=packet.master==\"joiner\"and\"joiner\"or\"initiator\",expires=math.min(expires,timestamp+DISCOVERY_TTL)}trace(\"discovery\",\"announcement_received\",{from=from,label=packet.label,distance=distance})local value,existing=config(),config().peers[from]if existing and not existing.rekeyRequired then markRejected(\"computer is already a direct peer\")elseif routes[from]and not existing then markRejected(\"computer is already a routed network member\")elseif discovery.state==\"searching\"then discovery.state,discovery.candidate,discovery.error=\"candidate\",discovered[packet.session],nil setLast(\"candidate\",\"matching computer found; confirm pairing\")trace(\"discovery\",\"candidate_found\",{from=from,label=packet.label,distance=distance})end end return end end local function handlePairJoin(packet,distance)if not discovery or discovery.state~=\"hosting\"or packet.session~=discovery.id then return end local from=tostring(packet.from)local value,existing=config(),config().peers[from]local repairingOfflineLink=existing and not existing.rekeyRequired and linkState(from)==\"offline\"if((existing and not existing.rekeyRequired and not repairingOfflineLink)or(routes[from]and not existing))then handshake(from,{type=\"pair_reject\",session=packet.session,reason=\"already_network_member\"})trace(\"discovery\",\"pair_rejected\",{from=from,reason=\"already_network_member\"})return end if type(packet.nonce)~=\"string\"or#packet.nonce<16 or#packet.nonce>128 or type(packet.proof)~=\"string\"or#packet.proof~=64 then return rejectPacket(\"malformed_pair_join\",packet,nil,distance)end local expected=joinProof(discovery.phrase,discovery.id,id(),from,packet.nonce)if not password.constantTimeEqual(expected,packet.proof)then discovery.failures=(discovery.failures or 0)+1 if discovery.failures>=5 then markRejected(\"too many invalid pairing proofs\")end handshake(from,{type=\"pair_reject\",session=packet.session,reason=\"phrase_mismatch\"})trace(\"discovery\",\"pair_rejected\",{from=from,reason=\"phrase_mismatch\"})return end local masterId=discovery.mode==\"master\"and(discovery.masterSide==\"initiator\"and id()or from)or\"\"local key,keyErr=linkKey(discovery.phrase,discovery.id,id(),from,packet.nonce)if not key then return markRejected(keyErr)end local persisted,persistErr=persistPair(from,key,{mode=discovery.mode,label=packet.label},masterId)if not persisted then handshake(from,{type=\"pair_reject\",session=packet.session,reason=\"persistence_failed\"})trace(\"discovery\",\"pair_rejected\",{from=from,reason=\"persistence_failed\",error=persistErr})markRejected(\"could not save pairing\")return false end local record={id=discovery.id,peer=from,joinNonce=packet.nonce,mode=discovery.mode,masterId=masterId,label=label(),key=key,keyId=keyId(key),expires=now()+DISCOVERY_TTL}record.confirmProof=confirmProof(discovery.phrase,record.id,id(),from,record.joinNonce,record.mode,record.masterId)discovery=nil setLast(\"paired\",\"awaiting pairing acknowledgement\")trace(\"discovery\",repairingOfflineLink and\"pair_repair_verified\"or\"pair_join_verified\",{from=from,distance=distance})return queueConfirmation(from,record)end local function handlePairConfirm(packet,distance)if not discovery or discovery.state~=\"confirming\"or not discovery.candidate or packet.session~=discovery.candidate.session then return end local from=tostring(packet.from)if from~=tostring(discovery.candidate.from)or packet.nonce~=discovery.joinNonce then return rejectPacket(\"unexpected_pair_confirmation\",packet,nil,distance)end local expected=confirmProof(discovery.phrase,packet.session,from,id(),discovery.joinNonce,packet.mode==\"master\"and\"master\"or\"peer\",packet.master or\"\")if not password.constantTimeEqual(expected,packet.proof or\"\")then return markRejected(\"pair confirmation proof did not match\")end local key,err=linkKey(discovery.phrase,packet.session,from,id(),discovery.joinNonce)if not key then return markRejected(err)end local persisted,persistErr=persistPair(from,key,{mode=packet.mode,label=packet.label},packet.master)if not persisted then handshake(from,{type=\"pair_reject\",session=packet.session,reason=\"persistence_failed\",proof=password.hmacSha256(key,\"ceetos/pair/reject/v2|\"..packet.session..\"|\"..id()..\"|\"..from..\"|persistence_failed\")})trace(\"discovery\",\"pair_persistence_failed\",{from=from,error=persistErr})markRejected(\"could not save pairing\")return false end local sessionId=packet.session discovery=nil setLast(\"paired\",\"paired with \"..tostring(packet.label or from))handshake(from,{type=\"pair_ack\",session=sessionId,proof=password.hmacSha256(key,\"ceetos/pair/ack/v2|\"..sessionId)})meshEnrollmentSends[tostring(from)]=nil ensureMeshEnrollment(now())trace(\"discovery\",\"pair_confirmed\",{from=from,distance=distance})return true end local function handlePairAck(packet)local record=confirmations[packet.session]if not record or tostring(record.peer)~=tostring(packet.from)then return end local value,peer=config(),activePeer(config(),record.peer)if not peer then return end local expected=password.hmacSha256(peer.key,\"ceetos/pair/ack/v2|\"..packet.session)if password.constantTimeEqual(expected,packet.proof or\"\")then confirmations[packet.session]=nil setLast(\"paired\",\"paired with \"..tostring(record.peer))meshEnrollmentSends[tostring(record.peer)]=nil ensureMeshEnrollment(now())routesDirty=true trace(\"discovery\",\"pair_acknowledged\",{from=packet.from})end end local function handleRouteAdvertisement(from,packet)local body=packet.body if type(body)~=\"table\"or type(body.sequence)~=\"number\"or type(body.members)~=\"table\"or(body.withdrawals~=nil and type(body.withdrawals)~=\"table\")then return rejectPacket(\"malformed_route_advertisement\",packet)end local key=tostring(from)M._routeSequences=M._routeSequences or{}local session=body.session if session~=nil and(type(session)~=\"string\"or#session<16 or#session>128)then return rejectPacket(\"invalid_route_advertisement_session\",packet)end session=session or\"legacy\"local previous=M._routeSequences[key]local sessionChanged=type(previous)==\"table\"and previous.session~=session if sessionChanged and body.full~=true then return rejectPacket(\"route_advertisement_session_requires_full_snapshot\",packet)end if type(previous)==\"table\"and previous.session==session and previous.sequence>=body.sequence then return end if type(previous)==\"number\"and session==\"legacy\"and previous>=body.sequence then return end M._routeSequences[key]={session=session,sequence=body.sequence}local value,changed,count=config(),false,0 if sessionChanged then for _,bucket in pairs(routes)do if type(bucket.candidates)==\"table\"then local kept={}for _,candidate in ipairs(bucket.candidates)do if tostring(candidate.next)~=key then kept[#kept+1]=candidate else changed=true end end bucket.candidates=kept end end trace(\"routing\",\"route_advertisement_session_reset\",{from=from})end if body.controller and not value.controller then value.controller,changed=tostring(body.controller),true end local announced={}for advertisedId,meta in pairs(body.members)do count=count+1 if count>MAX_PACKET_TABLE then return rejectPacket(\"route_advertisement_too_large\",packet)end local peerId=tostring(advertisedId)announced[peerId]=true if peerId~=id()and peerId~=key and type(meta)==\"table\"then local hops=math.floor(tonumber(meta.hops)or 0)+1 if hops>1 and hops<=PACKET_TTL and type(meta.label)~=\"table\"and type(meta.mode)~=\"table\"then local bucket,found=routeBucket(peerId),nil for _,candidate in ipairs(bucket.candidates)do if tostring(candidate.next)==key then found=candidate;break end end if found then local metadataChanged=found.hops~=hops or found.label~=meta.label or found.mode~=meta.mode or tostring(found.masterId or\"\")~=tostring(meta.masterId or\"\")or found.kind~=(meta.kind or\"route\")found.hops,found.expires=hops,now()+ROUTE_TTL found.label,found.mode,found.masterId,found.kind=meta.label,meta.mode,meta.masterId,meta.kind or\"route\"if metadataChanged then changed=true end else bucket.candidates[#bucket.candidates+1]={next=key,hops=hops,expires=now()+ROUTE_TTL,label=type(meta.label)==\"string\"and meta.label:sub(1,128)or nil,mode=meta.mode==\"master\"and\"master\"or\"peer\",masterId=meta.masterId and tostring(meta.masterId)or nil,kind=meta.kind==\"mesh\"and\"mesh\"or\"route\"}table.sort(bucket.candidates,compareCandidates)while#bucket.candidates>ROUTE_CANDIDATE_LIMIT do table.remove(bucket.candidates)end changed=true end end end end local withdrawals=body.withdrawals or{}for _,withdrawnId in pairs(withdrawals)do count=count+1 if count>MAX_PACKET_TABLE then return rejectPacket(\"route_advertisement_too_large\",packet)end local bucket=routes[tostring(withdrawnId)]if bucket and type(bucket.candidates)==\"table\"then local kept={}for _,candidate in ipairs(bucket.candidates)do if tostring(candidate.next)~=key then kept[#kept+1]=candidate else changed=true end end bucket.candidates=kept end end if body.full==true then for peerId,bucket in pairs(routes)do if type(bucket.candidates)==\"table\"and not announced[tostring(peerId)]then local kept={}for _,candidate in ipairs(bucket.candidates)do if tostring(candidate.next)~=key then kept[#kept+1]=candidate else changed=true end end bucket.candidates=kept end end end if changed then if value.controller then save(value)end;routesDirty=true;trace(\"routing\",\"route_table_updated\",{from=from})end end local function handleProbe(from,packet)local token=type(packet.body)==\"table\"and packet.body.token if type(token)~=\"string\"or#token<16 or#token>128 then return rejectPacket(\"malformed link probe\",packet)end touchLink(from,nil,\"probe\")return send(from,{type=\"mesh_probe_ack\",body={token=token},request_id=newRequestId()},from)end local function handleProbeAck(from,packet,distance)local health,token=healthFor(from),type(packet.body)==\"table\"and packet.body.token if not health.awaiting or token~=health.probeToken then return rejectPacket(\"unexpected link probe acknowledgement\",packet)end local sample=math.max(0,now()-(health.lastProbe or now()))health.rtt=health.rtt and math.floor((health.rtt*3+sample)/4)or sample health.awaiting,health.misses,health.probeToken=false,0,nil touchLink(from,distance,\"probe_ack\")if autoEdges[from]then autoEdges[from].expires=now()+ROUTE_TTL end trace(\"routing\",\"packet_received\",{type=packet.type,from=from,destination=packet.destination,distance=distance})end function M.handle(event)if event[1]==\"peripheral\"or event[1]==\"peripheral_detach\"then M.invalidateTransport()openChannels(config())return true end if event[1]~=\"modem_message\"then return end local channel,packet,distance=event[3],event[5],event[6]local expected=false for _,item in ipairs(transport.channels or{})do if channel==item then expected=true;break end end if not expected then return rejectPacket(\"unexpected_channel\",packet,channel,distance)end transport.lastReceive,transport.lastDistance=now(),distance if type(packet)~=\"table\"then return rejectPacket(\"packet_not_table\",packet,channel,distance)end if packet.protocol==PUBLIC_UPDATE_PROTOCOL then if channel~=DISCOVERY_CHANNEL or not PUBLIC_UPDATE_TYPES[packet.type]or type(packet.from)~=\"string\"and type(packet.from)~=\"number\"or not(isForUs(packet.to)or isBroadcast(packet.to))then return rejectPacket(\"invalid_public_update_packet\",packet,channel,distance)end local encoded,encodeErr=canonical(packet.body or{})if not encoded then return rejectPacket(\"invalid_public_update_body:\"..tostring(encodeErr),packet,channel,distance)end trace(\"transport\",\"public_update_received\",{type=packet.type,from=packet.from,distance=distance,bytes=#encoded})if M.publicHandler then return M.publicHandler(packet,distance)end return end local discoveryPacket=packet.type==\"discover\"or packet.type==\"discover_query\"or packet.type==\"discover_cancel\"local handshakePacket=packet.type==\"pair_join\"or packet.type==\"pair_confirm\"or packet.type==\"pair_ack\"or packet.type==\"pair_reject\"local meshPacketType=packet.type==\"mesh_beacon\"or packet.type==\"mesh_edge_offer\"or packet.type==\"mesh_edge_accept\"if discoveryPacket then if packet.protocol~=PROTOCOL or channel~=DISCOVERY_CHANNEL then return rejectPacket(\"invalid_discovery_transport\",packet,channel,distance)end return handleDiscovery(packet,channel,distance)end if handshakePacket then if not isForUs(packet.to)then return end if packet.protocol~=PROTOCOL or channel~=DISCOVERY_CHANNEL then return rejectPacket(\"invalid_handshake_transport\",packet,channel,distance)end if packet.type==\"pair_join\"then return handlePairJoin(packet,distance)elseif packet.type==\"pair_confirm\"then return handlePairConfirm(packet,distance)elseif packet.type==\"pair_ack\"then return handlePairAck(packet)elseif packet.type==\"pair_reject\"then if packet.reason==\"persistence_failed\"and rollbackPendingConfirmation(packet)then return end if discovery then markRejected(\"pairing rejected: \"..tostring(packet.reason or\"remote peer rejected it\"));return end end return end if meshPacketType then if packet.protocol~=PROTOCOL or channel~=(config().channel or CHANNEL)then return rejectPacket(\"invalid mesh transport\",packet,channel,distance)end if packet.type~=\"mesh_beacon\"and not isForUs(packet.to)then return end return handleMeshPacket(packet,distance)end if not isForUs(packet.to)then return end local value=config()local valid,peerOrReason=verifyPacket(packet,value)if not valid then return rejectPacket(peerOrReason,packet,channel,distance)end local peer,from=peerOrReason,tostring(packet.from)if packet.confidential==true then local opened,openErr=openBody(peer,packet)if not opened then return rejectPacket(openErr or\"could not open confidential packet\",packet,channel,distance)end packet.body=opened end if packet.type==\"mesh_probe_ack\"then return handleProbeAck(from,packet,distance)end touchLink(from,distance,\"authenticated_packet\")if autoEdges[from]then autoEdges[from].expires=now()+ROUTE_TTL end trace(\"routing\",\"packet_received\",{type=packet.type,from=from,destination=packet.destination,distance=distance})if packet.type==\"mesh_enroll\"then return acceptEnrollment(from,peer,packet)end if packet.type==\"mesh_probe\"then return handleProbe(from,packet)end if packet.type==\"route_advertise\"then return handleRouteAdvertisement(from,packet)end if packet.reply_to and packet.destination and tostring(packet.destination)~=id()then local reverse=reverseRoutes[packet.reply_to]if not reverse or reverse.expires<=now()or reverse.origin~=tostring(packet.destination)then return rejectPacket(\"reverse reply route unavailable\",packet,channel,distance)end local requestId=tostring(packet.request_id or\"\")if requestId==\"\"or not admitSeenPacket(requestId)then return trace(\"routing\",\"forward_duplicate\",{from=from,type=packet.type})end if(tonumber(packet.ttl)or 0)<=1 then return rejectPacket(\"TTL exhausted\",packet,channel,distance)end packet.ttl=packet.ttl-1 trace(\"routing\",\"reverse_reply_forwarded\",{from=from,to=reverse.previous,type=packet.type})return send(packet.destination,packet,reverse.previous)end if packet.destination and tostring(packet.destination)~=id()then local requestId=tostring(packet.request_id or\"\")if requestId==\"\"or not admitSeenPacket(requestId)then return trace(\"routing\",\"forward_duplicate\",{from=from,type=packet.type})end local remembered,rememberErr=rememberReverse(packet,from)if not remembered then return rejectPacket(rememberErr or\"could not record reverse route\",packet,channel,distance)end if(tonumber(packet.ttl)or 0)<=1 then return rejectPacket(\"TTL exhausted\",packet,channel,distance)end packet.ttl=packet.ttl-1 return send(packet.destination,packet)end local requestId=tostring(packet.request_id or\"\")if requestId==\"\"then return rejectPacket(\"missing request ID\",packet,channel,distance)end if not admitSeenPacket(requestId)then return trace(\"routing\",\"destination_duplicate\",{from=from,type=packet.type})end local remembered,rememberErr=rememberReverse(packet,from)if not remembered then return rejectPacket(rememberErr or\"could not record reverse route\",packet,channel,distance)end packet.authenticatedHop,packet.originVerified=from,false if M.handler then local handled,handlerErr=M.handler(from,peer,packet)if handled==false then trace(\"routing\",\"handler_rejected\",{type=packet.type,from=from,error=tostring(handlerErr or\"handler rejected\"):sub(1,120)})elseif handled==nil then trace(\"routing\",\"handler_unhandled\",{type=packet.type,from=from})end return handled,handlerErr end end return M",
  ["ceetos/lib/network_ipc.lua"] = "local store=require(\"ceetos.lib.store\")local M={}local ROOT=\"/ceetos/data/network-ipc\"local COMMANDS=ROOT..\"/commands\"local RESULTS=ROOT..\"/results\"M.EVENT=\"ceetos_network_ipc\"M.RESULT_EVENT=\"ceetos_network_ipc_result\"local function ensure(path)if not fs.exists(path)then fs.makeDir(path)end end local function safeId(value)return type(value)==\"string\"and value:match(\"^[%w_%-%.]+$\")and#value<=160 end local function path(directory,requestId)return fs.combine(directory,requestId..\".lua\")end function M.submit(request)if type(request)~=\"table\"or not safeId(request.id)then return false,\"invalid IPC request ID\"end ensure(ROOT);ensure(COMMANDS);ensure(RESULTS)local target=path(COMMANDS,request.id)if fs.exists(target)then return false,\"IPC request already exists\"end local ok,err=store.write(target,request)if ok and os and os.queueEvent then pcall(os.queueEvent,M.EVENT,request.id)end return ok,err end function M.take()ensure(ROOT);ensure(COMMANDS);ensure(RESULTS)local names=fs.list(COMMANDS)table.sort(names)for _,name in ipairs(names)do local requestId=name:match(\"^([%w_%-%.]+)%.lua$\")if requestId and safeId(requestId)then local target=fs.combine(COMMANDS,name)store.invalidate(target)local request=store.readFresh(target,nil)fs.delete(target)if type(request)==\"table\"and request.id==requestId then return request end end end end function M.pending()ensure(ROOT);ensure(COMMANDS);ensure(RESULTS)for _,name in ipairs(fs.list(COMMANDS))do local requestId=name:match(\"^([%w_%-%.]+)%.lua$\")if requestId and safeId(requestId)then return true end end return false end function M.reply(requestId,value)if not safeId(requestId)or type(value)~=\"table\"then return false,\"invalid IPC result\"end ensure(ROOT);ensure(COMMANDS);ensure(RESULTS)local ok,err=store.write(path(RESULTS,requestId),value)if ok then if os and os.queueEvent then pcall(os.queueEvent,M.RESULT_EVENT,requestId)end return true end local compact={id=requestId,ok=false,error=\"could not encode IPC result: \"..tostring(err or\"unknown error\"):sub(1,160),}local fallbackOk,fallbackErr=store.write(path(RESULTS,requestId),compact)if fallbackOk then if os and os.queueEvent then pcall(os.queueEvent,M.RESULT_EVENT,requestId)end return true end return false,tostring(err or fallbackErr or\"could not commit IPC result\")end local function readResult(target,expectedId)local handle=fs.open(target,\"r\")if not handle then return nil,\"could not open committed IPC result\"end local raw=handle.readAll()handle.close()local ok,result=pcall(textutils.unserialise,raw)if not ok or type(result)~=\"table\"then return nil,\"invalid committed IPC result encoding\"end if result.id~=expectedId then return nil,\"IPC result request ID mismatch\"end return result end function M.poll(requestId,consume)if not safeId(requestId)then return nil,\"invalid IPC request ID\"end local target=path(RESULTS,requestId)local result,directErr=readResult(target,requestId)if result then if consume then fs.delete(target)end return result end if fs.exists(target)then return nil,directErr end for _,name in ipairs(fs.list(RESULTS))do if name:match(\"^[%w_%-%.]+%.lua$\")then local candidate=fs.combine(RESULTS,name)local handle=fs.open(candidate,\"r\")local raw=handle and handle.readAll()or nil if handle then handle.close()end local decoded,result=pcall(textutils.unserialise,raw or\"\")if decoded and type(result)==\"table\"and result.id==requestId then if consume then fs.delete(candidate)end return result end end end return nil end function M.await(requestId,timeout,options)if not safeId(requestId)then return nil,\"invalid IPC request ID\"end ensure(ROOT);ensure(COMMANDS);ensure(RESULTS)options=type(options)==\"table\"and options or{}local pullEvent=options.pullEvent or(os and os.pullEventRaw)local startTimer=options.startTimer or(os and os.startTimer)local now=options.now or function()return os.epoch(\"utc\")end if type(pullEvent)~=\"function\"or type(startTimer)~=\"function\"then return nil,\"event wait is unavailable\"end local duration=math.max(1,math.floor(tonumber(timeout)or 5000))local deadline=now()+duration local result,err=M.poll(requestId,true)if result or err then return result,err end local timer=startTimer(duration/1000)while true do local event={pullEvent()}if event[1]==M.RESULT_EVENT and tostring(event[2]or\"\")==requestId then result,err=M.poll(requestId,true)if result or err then return result,err end elseif event[1]==\"timer\"and event[2]==timer then result,err=M.poll(requestId,true)if result or err then return result,err end return nil,\"CeetOS network service did not respond\"end if now()>=deadline then result,err=M.poll(requestId,true)if result or err then return result,err end return nil,\"CeetOS network service did not respond\"end end end function M.prune(limit)ensure(ROOT);ensure(COMMANDS);ensure(RESULTS)limit=math.max(1,math.floor(tonumber(limit)or 32))local names={}for _,name in ipairs(fs.list(RESULTS))do if name:match(\"^[%w_%-%.]+%.lua$\")then names[#names+1]=name end end table.sort(names)while#names>limit do fs.delete(fs.combine(RESULTS,table.remove(names,1)))end end function M.status()ensure(ROOT);ensure(COMMANDS);ensure(RESULTS)local function count(directory)local total=0 for _,name in ipairs(fs.list(directory))do if name:match(\"^[%w_%-%.]+%.lua$\")then total=total+1 end end return total end return{commands=count(COMMANDS),results=count(RESULTS)}end function M.inspect(limit)ensure(ROOT);ensure(COMMANDS);ensure(RESULTS)limit=math.max(1,math.min(16,math.floor(tonumber(limit)or 8)))local function ids(directory)local out={}for _,name in ipairs(fs.list(directory))do local requestId=name:match(\"^([%w_%-%.]+)%.lua$\")if requestId and safeId(requestId)then out[#out+1]=requestId end end table.sort(out)while#out>limit do table.remove(out,1)end return out end local commandIds,resultIds=ids(COMMANDS),ids(RESULTS)return{commands=#commandIds,results=#resultIds,commandIds=commandIds,resultIds=resultIds}end return M",
  ["ceetos/lib/devbridge_policy.lua"] = "local M={}function M.isLoopbackUrl(url)return type(url)==\"string\"and(url:match(\"^wss?://127%.0%.0%.1[:/]\")or url:match(\"^wss?://localhost[:/]\")or url:match(\"^wss?://%[::1%][:/]\"))~=nil end function M.allowsUnattendedCraftProbe(connection,lease,profileId,now)if profileId~=\"desktop\"or type(connection)~=\"table\"or type(lease)~=\"table\"then return false end if connection.autoConnect~=true or(connection.source~=\"devbuild-installer\"and connection.source~=\"devbuild-recovery\")then return false end if connection.token~=nil and connection.token~=\"\"then return false end if not M.isLoopbackUrl(connection.url)then return false end return tonumber(lease.untilAt or 0)>tonumber(now or 0)end function M.isCraftProbeCommand(command)if type(command)~=\"table\"or command.devProbe~=true or type(command.token)~=\"string\"then return false end if not command.token:match(\"^dev%-e2e%-%d+%-%d+%-%d%d$\")then return false end if command.action==\"mode\"then return command.value==true end if command.action==\"destination\"then return type(command.value)==\"string\"and#command.value>0 and#command.value<=64 and not command.value:find(\"[%c]\")end if command.action==\"add\"then local count=tonumber(command.count)return type(command.name)==\"string\"and command.name:match(\"^[%w_.%-]+:[%w_./%-]+$\")~=nil and count and count>=1 and count<=256 and count==math.floor(count)end return command.action==\"submit\"end return M",
  ["ceetos/lib/service_tick.lua"] = "local M={}function M.new(intervalMs)return{interval=math.max(25,math.min(1000,math.floor(tonumber(intervalMs)or 250))),next=0,}end function M.due(state,timestamp,force)if type(state)~=\"table\"then return false end timestamp=math.max(0,math.floor(tonumber(timestamp)or 0))if force or timestamp>=(tonumber(state.next)or 0)then state.next=timestamp+math.max(25,math.floor(tonumber(state.interval)or 250))return true end return false end function M.claim(state,timestamp,explicit,pending,claim)if type(pending)~=\"function\"or type(claim)~=\"function\"then return false end if not M.due(state,timestamp,explicit)then return false end if not explicit and not pending()then return false end claim()return true end return M",
  ["ceetos/lib/network_runtime.lua"] = "local serviceTick=require(\"ceetos.lib.service_tick\")local M={}local Runtime={}Runtime.__index=Runtime local function interval(value,fallback)value=tonumber(value)or fallback return math.max(0.05,math.min(value,60))end local function bounded(value)return tostring(value or\"runtime callback failed\"):sub(1,240)end function M.new(options)options=options or{}assert(type(options.now)==\"function\",\"network runtime requires now\")assert(type(options.startTimer)==\"function\",\"network runtime requires startTimer\")assert(type(options.handle)==\"function\",\"network runtime requires a packet handler\")local tasks={}for _,task in ipairs(options.tasks or{})do assert(type(task)==\"table\"and type(task.name)==\"string\"and task.name~=\"\",\"invalid network runtime task\")assert(type(task.run)==\"function\",\"network runtime task requires run\")tasks[#tasks+1]={name=task.name,initial=interval(task.initial,task.interval or 1),interval=interval(task.interval,1),run=task.run,dueAt=0,runs=0,lastRun=nil,}end local ipc=options.ipc or{}local runtime=setmetatable({now=options.now,startTimer=options.startTimer,pullEvent=options.pullEvent,handle=options.handle,onEvent=type(options.onEvent)==\"function\"and options.onEvent or nil,onFault=type(options.onFault)==\"function\"and options.onFault or function()end,tasks=tasks,tasksByName={},taskTimers={},started=false,events=0,ipc={event=ipc.event,interval=interval(ipc.interval,0.5),timer=nil,pump=serviceTick.new(ipc.cadence or 250),pending=type(ipc.pending)==\"function\"and ipc.pending or function()return false end,claim=type(ipc.claim)==\"function\"and ipc.claim or function()end,},},Runtime)for _,task in ipairs(tasks)do runtime.tasksByName[task.name]=task end return runtime end function Runtime:report(scope,errorText)pcall(self.onFault,tostring(scope),bounded(errorText))end function Runtime:invoke(scope,callback,...)local arguments={...}local unpackArguments=table.unpack or unpack local ok,result=xpcall(function()return callback(unpackArguments(arguments))end,bounded)if not ok then self:report(scope,result)end return ok,result end function Runtime:schedule(task,delay)local seconds=interval(delay,task.interval)self.taskTimers[task.name]=self.startTimer(seconds)task.dueAt=self.now()+math.floor(seconds*1000)end function Runtime:reschedule(name,delay)local task=self.tasksByName[tostring(name or\"\")]if not task then return false,\"unknown network runtime task\"end self:schedule(task,delay)return true end function Runtime:start()if self.started then return end self.started=true self.ipc.timer=self.startTimer(self.ipc.interval)for _,task in ipairs(self.tasks)do self:schedule(task,task.initial)end end function Runtime:step(event)assert(type(event)==\"table\",\"network runtime event must be a table\")self:start()self.events=self.events+1 local explicitIpc=event[1]==self.ipc.event or(event[1]==\"timer\"and event[2]==self.ipc.timer)local function claim()local ok,result=self:invoke(\"ipc\",self.ipc.claim)return ok and result end local function pending()local ok,result=self:invoke(\"ipc.pending\",self.ipc.pending)return ok and result==true end serviceTick.claim(self.ipc.pump,self.now(),explicitIpc,pending,claim)self:invoke(\"packet\",self.handle,event)if event[1]==\"timer\"and event[2]==self.ipc.timer then self.ipc.timer=self.startTimer(self.ipc.interval)end if self.onEvent then self:invoke(\"event\",self.onEvent,event)end local timestamp=self.now()for _,task in ipairs(self.tasks)do local fired=event[1]==\"timer\"and event[2]==self.taskTimers[task.name]if fired or timestamp>=(tonumber(task.dueAt)or 0)then local ok,delay=self:invoke(\"task.\"..task.name,task.run,event)task.runs,task.lastRun=task.runs+1,timestamp self:schedule(task,ok and delay or task.interval)end end return true end function Runtime:run()assert(type(self.pullEvent)==\"function\",\"network runtime requires pullEvent to run\")self:start()while true do self:step({self.pullEvent()})end end function Runtime:status()local timers={}local tasks={}for name,timer in pairs(self.taskTimers)do timers[name]=timer end for _,task in ipairs(self.tasks)do tasks[task.name]={dueAt=task.dueAt,runs=task.runs,lastRun=task.lastRun}end return{started=self.started,events=self.events,ipcTimer=self.ipc.timer,taskTimers=timers,tasks=tasks}end return M",
  ["ceetos/lib/auth_client.lua"] = "local store=require(\"ceetos.lib.store\")local password=require(\"ceetos.lib.password\")local networkIpc=require(\"ceetos.lib.network_ipc\")local meshCrypto=require(\"ceetos.lib.mesh_crypto\")local M={}local REPLIES=\"/ceetos/data/auth-replies\"local DEFERRED=REPLIES..\"/deferred\"local PENDING_MARKERS=REPLIES..\"/pending\"local AUTH_REPLY_TIMEOUT,AUTH_BEGIN_ATTEMPTS,REPLY_RETENTION_MS=15000,3,90000 local AUTH_BEGIN_TIMEOUT=4000 local AUTH_SEND_ATTEMPTS,AUTH_SEND_RETRY_MS=3,1200 local DEFERRED_RETENTION_MS,DEFERRED_LIMIT=20000,16 local sequence=0 local pending={}local PENDING_LIMIT=32 local delivery={received=0,stored=0,pending=0,lastId=nil,lastAt=0,lastError=nil}local function now()return os.epoch(\"utc\")end local function authority()store.invalidate(\"/ceetos/data/auth-authority.lua\")local value=store.read(\"/ceetos/data/auth-authority.lua\",nil)return type(value)==\"table\"and value.active==true and value or nil end local function requestId(kind)sequence=sequence+1 return table.concat({\"auth\",kind,tostring(os.getComputerID()),tostring(now()),tostring(sequence)},\"-\")end local function validId(id)return type(id)==\"string\"and id:match(\"^[%w_%-%.]+$\")and#id<=160 end local function replyPath(id)return validId(id)and fs.combine(REPLIES,id..\".lua\")or nil end local function proofResultAad(authorityId,nodeId,requestId,challengeId,challenge)return table.concat({\"ceetos/auth/login/result/v2\",tostring(authorityId),tostring(nodeId),tostring(requestId),tostring(challengeId),tostring(challenge),},\"|\")end local function openRoutedLoginResult(body,expected)if tostring(body.authority or\"\")~=tostring(expected.authority)or tostring(body.node or\"\")~=tostring(expected.node)or tostring(body.challenge_id or\"\")~=tostring(expected.challengeId)then return nil,\"routed login reply does not match its request\"end if body.ok~=true then return false,tostring(body.error or\"Auth Server rejected login\"):sub(1,160)end if type(body.sealed)~=\"table\"then return nil,\"missing sealed routed login result\"end local envelope=body.sealed if type(envelope.nonce)~=\"string\"or#envelope.nonce~=24 or type(envelope.tag)~=\"string\"or#envelope.tag~=32 or type(envelope.ciphertext)~=\"string\"or#envelope.ciphertext>8192 then return nil,\"invalid routed login envelope\"end local aad=proofResultAad(expected.authority,expected.node,body.request_id,expected.challengeId,expected.challenge)local plaintext,err=meshCrypto.open(expected.verifier,envelope,aad)if not plaintext or#plaintext>4096 then return nil,err or\"could not open routed login result\"end local ok,result=pcall(textutils.unserialise,plaintext)if not ok or type(result)~=\"table\"or type(result.session)~=\"table\"then return nil,\"invalid routed login result\"end return result end local function ensureReplies()if not fs.exists(REPLIES)then fs.makeDir(REPLIES)end if not fs.exists(DEFERRED)then fs.makeDir(DEFERRED)end if not fs.exists(PENDING_MARKERS)then fs.makeDir(PENDING_MARKERS)end end local function markerPath(id)return validId(id)and fs.combine(PENDING_MARKERS,id..\".lua\")or nil end local function clearMarker(id)local path=markerPath(id)if path and fs.exists(path)then pcall(fs.delete,path)end end local function readMarker(id)local path=markerPath(id)if not path or not fs.exists(path)then return nil end local handle=fs.open(path,\"r\")local raw=handle and handle.readAll()or nil if handle then handle.close()end local ok,marker=pcall(textutils.unserialise,raw or\"\")if not ok or type(marker)~=\"table\"or marker.schema~=1 or marker.request_id~=id or type(marker.type)~=\"string\"or type(marker.authority)~=\"string\"or(tonumber(marker.expires)or 0)<=now()then clearMarker(id)return nil end return marker end local function writeMarker(id,expected)ensureReplies()local path=markerPath(id)if not path or type(expected)~=\"table\"then return false,\"invalid authority request marker\"end local marker={schema=1,request_id=id,type=expected.type,authority=tostring(expected.authority),expires=tonumber(expected.expires)or 0}if(marker.type~=\"auth_login_begin_result\"and marker.type~=\"auth_login_proof_result\"and marker.type~=\"auth_mutate_result\")or marker.expires<=now()then return false,\"invalid authority request marker\"end local temporary=path..\".tmp\"if fs.exists(temporary)then fs.delete(temporary)end local handle=fs.open(temporary,\"w\")if not handle then return false,\"could not store authority request marker\"end handle.write(textutils.serialise(marker));handle.close()if fs.exists(path)then fs.delete(path)end fs.move(temporary,path)return true end local function pruneDeferred()local timestamp,markerRows,rows=now(),{},{}if fs.exists(PENDING_MARKERS)then for _,name in ipairs(fs.list(PENDING_MARKERS))do local id=name:match(\"^([%w_%-%.]+)%.lua$\")if id then local marker=readMarker(id)if marker then markerRows[#markerRows+1]={id=id,expires=tonumber(marker.expires)or 0}end end end end table.sort(markerRows,function(a,b)return a.expires>b.expires end)for index=PENDING_LIMIT+1,#markerRows do clearMarker(markerRows[index].id)end if not fs.exists(DEFERRED)then return end for _,name in ipairs(fs.list(DEFERRED))do local id=name:match(\"^([%w_%-%.]+)%.lua$\")if id then local path=fs.combine(DEFERRED,name)local handle=fs.open(path,\"r\")local raw=handle and handle.readAll()or nil if handle then handle.close()end local ok,row=pcall(textutils.unserialise,raw or\"\")local valid=ok and type(row)==\"table\"and row.schema==1 and row.request_id==id and type(row.body)==\"table\"and row.body.request_id==id and type(row.context)==\"table\"and type(row.context.type)==\"string\"and tonumber(row.at)and tonumber(row.at)+DEFERRED_RETENTION_MS>timestamp and readMarker(id)~=nil if valid then rows[#rows+1]={path=path,at=tonumber(row.at)}else fs.delete(path)end end end table.sort(rows,function(a,b)return a.at>b.at end)for index=DEFERRED_LIMIT+1,#rows do fs.delete(rows[index].path)end end local function prunePending()local timestamp,rows=now(),{}for id,entry in pairs(pending)do if type(entry)~=\"table\"or(tonumber(entry.expires)or 0)<=timestamp then pending[id]=nil clearMarker(id)else rows[#rows+1]={id=id,expires=tonumber(entry.expires)or 0}end end table.sort(rows,function(a,b)return a.expires>b.expires end)for index=PENDING_LIMIT+1,#rows do pending[rows[index].id]=nil;clearMarker(rows[index].id)end end local function pruneReplies()if not fs.exists(REPLIES)then return end local rows={}for _,name in ipairs(fs.list(REPLIES))do local id=name:match(\"^([%w_%-%.]+)%.lua$\")if id then local path=fs.combine(REPLIES,name)local handle=fs.open(path,\"r\")local raw=handle and handle.readAll()or nil if handle then handle.close()end local ok,decoded=pcall(textutils.unserialise,raw or\"\")local at=ok and type(decoded)==\"table\"and tonumber(decoded.at)or 0 if at<=0 or at+REPLY_RETENTION_MS<=now()then fs.delete(path)else rows[#rows+1]={path=path,at=at}end end end table.sort(rows,function(a,b)return a.at>b.at end)for index=17,#rows do fs.delete(rows[index].path)end end local function take(id)local path=replyPath(id)if not path or not fs.exists(path)then return nil end local handle=fs.open(path,\"r\")if not handle then return nil end local raw=handle.readAll();handle.close()local ok,row=pcall(textutils.unserialise,raw)if not ok or type(row)~=\"table\"or row.request_id~=id then return nil end fs.delete(path)return row end local function deferredPath(id)return validId(id)and fs.combine(DEFERRED,id..\".lua\")or nil end local function clearDeferred(id)local path=deferredPath(id)if path and fs.exists(path)then fs.delete(path)end end local function safeReplyContext(context)context=type(context)==\"table\"and context or{}return{from=type(context.from)==\"string\"and context.from:sub(1,64)or\"\",direct=context.direct==true,routed=context.routed==true,type=type(context.type)==\"string\"and context.type:sub(1,64)or\"\",deferred=false,}end local function deferInbound(body,context)if type(body)~=\"table\"or type(body.request_id)~=\"string\"or not validId(body.request_id)then return false,\"invalid authority reply\"end local typeName=type(context)==\"table\"and context.type or nil if typeName~=\"auth_login_begin_result\"and typeName~=\"auth_login_proof_result\"and typeName~=\"auth_mutate_result\"then return false,\"invalid authority reply type\"end ensureReplies()pruneDeferred()local path=deferredPath(body.request_id)if not path then return false,\"invalid authority reply ID\"end local marker=readMarker(body.request_id)if not marker or marker.type~=typeName then return false,\"unsolicited authority reply\"end if context.routed~=true and(context.direct~=true or tostring(context.from or\"\")~=marker.authority)then return false,\"untrusted authority reply\"end local envelope={schema=1,request_id=body.request_id,body=body,context=safeReplyContext(context),at=now()}local serialOk,encoded=pcall(textutils.serialise,envelope)if not serialOk or type(encoded)~=\"string\"or#encoded>12288 then return false,\"authority reply exceeds hand-off limit\"end if fs.exists(path)then return true end local temporary=path..\".tmp\"if fs.exists(temporary)then fs.delete(temporary)end local handle=fs.open(temporary,\"w\")if not handle then return false,\"could not store authority reply\"end handle.write(encoded);handle.close()if fs.exists(path)then fs.delete(temporary);return true end fs.move(temporary,path)return true end local function takeDeferred(id)pruneDeferred()local path=deferredPath(id)if not path or not fs.exists(path)then return nil end local handle=fs.open(path,\"r\")if not handle then return nil end local raw=handle.readAll();handle.close()fs.delete(path)local ok,row=pcall(textutils.unserialise,raw)if not ok or type(row)~=\"table\"or row.schema~=1 or row.request_id~=id or type(row.body)~=\"table\"or row.body.request_id~=id or type(row.context)~=\"table\"then return nil end return row end function M.receive(body,context)delivery.received=delivery.received+1 delivery.lastAt=now()if type(body)~=\"table\"or type(body.request_id)~=\"string\"or#body.request_id>160 then delivery.lastError=\"invalid authority reply\"return false,delivery.lastError end local path=replyPath(body.request_id)delivery.lastId=body.request_id if not path then delivery.lastError=\"invalid authority reply ID\"return false,delivery.lastError end prunePending()local expected=pending[body.request_id]if not expected then if type(context)==\"table\"and context.deferred==true then local stored,storeErr=deferInbound(body,context)if stored then if os and os.queueEvent then pcall(os.queueEvent,\"ceetos_auth_reply\",body.request_id)end delivery.stored,delivery.pending,delivery.lastError=delivery.stored+1,delivery.pending+1,nil return true end delivery.lastError=storeErr or\"could not defer authority reply\"return false,delivery.lastError end delivery.lastError=\"unsolicited authority reply\"return false,delivery.lastError end if(tonumber(expected.expires)or 0)<=now()then pending[body.request_id]=nil clearMarker(body.request_id)delivery.lastError=\"expired authority reply\"return false,delivery.lastError end if type(context)~=\"table\"or context.type~=expected.type then delivery.lastError=\"untrusted authority reply\"return false,delivery.lastError end if expected.routedLogin==true and context.routed==true then local result,openErr=openRoutedLoginResult(body,expected)if result==false then pending[body.request_id]=nil clearMarker(body.request_id)ensureReplies()local ok,err=store.write(path,{request_id=body.request_id,ok=false,error=openErr,at=now()})if not ok then delivery.lastError=tostring(err or\"could not store authority reply\");return false,delivery.lastError end delivery.stored,delivery.pending,delivery.lastError=delivery.stored+1,delivery.pending+1,nil return true end if not result then delivery.lastError=openErr return false,delivery.lastError end body={request_id=body.request_id,ok=true,result=result}elseif expected.routedChallenge==true and context.routed==true then if body.ok==true and(type(body.result)~=\"table\"or tostring(body.result.authority or\"\")~=tostring(expected.authority))then delivery.lastError=\"routed login challenge does not match authority\"return false,delivery.lastError end elseif context.direct~=true or tostring(context.from)~=tostring(expected.authority)then delivery.lastError=\"untrusted authority reply\"return false,delivery.lastError end pending[body.request_id]=nil clearMarker(body.request_id)ensureReplies()pruneReplies()local ok,err=store.write(path,{request_id=body.request_id,ok=body.ok==true,result=body.result,error=body.error,at=now()})if not ok then delivery.lastError=tostring(err or\"could not store authority reply\")return false,delivery.lastError end if os and os.queueEvent then pcall(os.queueEvent,\"ceetos_auth_reply\",body.request_id)end delivery.stored,delivery.pending,delivery.lastError=delivery.stored+1,delivery.pending+1,nil local rows={}for _,name in ipairs(fs.list(REPLIES))do local id=name:match(\"^([%w_%-%.]+)%.lua$\")if id then local candidate=fs.combine(REPLIES,name)local handle=fs.open(candidate,\"r\")local raw=handle and handle.readAll()or nil if handle then handle.close()end local decoded,parsed=pcall(textutils.unserialise,raw or\"\")if not decoded then parsed=nil end rows[#rows+1]={path=candidate,at=type(parsed)==\"table\"and tonumber(parsed.at)or 0}end end table.sort(rows,function(a,b)return a.at>b.at end)for index=33,#rows do fs.delete(rows[index].path)end return true end function M.deliveryStatus()pruneReplies()pruneDeferred()local pending,deferred=0,0 if fs.exists(REPLIES)then for _,name in ipairs(fs.list(REPLIES))do if name:match(\"^[%w_%-%.]+%.lua$\")then pending=pending+1 end end end if fs.exists(DEFERRED)then for _,name in ipairs(fs.list(DEFERRED))do if name:match(\"^[%w_%-%.]+%.lua$\")then deferred=deferred+1 end end end delivery.pending=pending return{received=delivery.received,stored=delivery.stored,pending=pending,lastId=delivery.lastId,lastAt=delivery.lastAt,lastError=delivery.lastError,deferred=deferred,}end local function send(typeName,body,timeout,expectation,retryAttempts)local server=authority()if not server then return nil,\"no Auth Server is reachable\"end local id=body.request_id or requestId(typeName)body.request_id=id prunePending()local expectedType=typeName..\"_result\"local pendingRecord={authority=tostring(server.id),type=expectedType,expires=now()+(timeout or AUTH_REPLY_TIMEOUT)}if type(expectation)==\"table\"then for key,value in pairs(expectation)do pendingRecord[key]=value end end local marked,markerErr=writeMarker(id,pendingRecord)if not marked then return nil,markerErr or\"could not prepare authority request\"end pending[id]=pendingRecord local deadline=now()+(timeout or AUTH_REPLY_TIMEOUT)local maxAttempts=math.max(1,math.min(AUTH_SEND_ATTEMPTS,tonumber(retryAttempts)or 1))local attempts,commandIds,nextAttemptAt,lastSendError=0,{},now(),nil local function submitAttempt()if attempts>=maxAttempts then return end attempts=attempts+1 local commandId=\"auth-\"..id..(attempts==1 and\"\"or(\"-retry-\"..attempts))local command={id=commandId,action=\"auth_send\",target=server.id,type=typeName,body=body}local ok,err=networkIpc.submit(command)if ok then commandIds[#commandIds+1]=commandId else lastSendError=err or\"could not reach CeetOS network service\"end nextAttemptAt=now()+AUTH_SEND_RETRY_MS end while now()<deadline do if attempts==0 or(attempts<maxAttempts and now()>=nextAttemptAt)then submitAttempt()end for index=#commandIds,1,-1 do local commandId=commandIds[index]local ack,pollErr=networkIpc.poll(commandId,true)if pollErr then pending[id]=nil;clearMarker(id);clearDeferred(id)return nil,pollErr end if ack then table.remove(commandIds,index)if ack.ok~=true then lastSendError=ack.error or\"Auth Server request was not sent\"end end end local reply=take(id)if reply then delivery.pending=math.max(0,delivery.pending-1)return reply.ok and reply.result or nil,reply.error or\"Auth Server rejected request\"end local deferred=takeDeferred(id)if deferred then M.receive(deferred.body,deferred.context)end sleep(0.05)end pending[id]=nil clearMarker(id)clearDeferred(id)return nil,lastSendError or\"Auth Server did not respond\"end local loginProof local function startRequest(typeName,body,timeout,expectation,retryAttempts)local server=authority()if not server then return nil,\"no Auth Server is reachable\"end local id=body.request_id or requestId(typeName)body.request_id=id prunePending()local duration=timeout or AUTH_REPLY_TIMEOUT local pendingRecord={authority=tostring(server.id),type=typeName..\"_result\",expires=now()+duration}if type(expectation)==\"table\"then for key,value in pairs(expectation)do pendingRecord[key]=value end end local marked,markerErr=writeMarker(id,pendingRecord)if not marked then return nil,markerErr or\"could not prepare authority request\"end pending[id]=pendingRecord return{id=id,type=typeName,body=body,server=server,pending=pendingRecord,deadline=now()+duration,attempts=0,commandIds={},maxAttempts=math.max(1,math.min(AUTH_SEND_ATTEMPTS,tonumber(retryAttempts)or 1)),nextAttemptAt=now(),lastSendError=nil,}end local function finishRequest(state,result,errorText)pending[state.id]=nil clearMarker(state.id)clearDeferred(state.id)state.done,state.result,state.error=true,result,errorText return result,errorText end local function tickRequest(state,event,eventId)if state.done then return state.result,state.error end local timestamp=now()local submittedNow=false if state.attempts<state.maxAttempts and timestamp>=state.nextAttemptAt then state.attempts=state.attempts+1 local commandId=\"auth-\"..state.id..(state.attempts==1 and\"\"or(\"-retry-\"..state.attempts))local submitted,submitErr=networkIpc.submit({id=commandId,action=\"auth_send\",target=state.server.id,type=state.type,body=state.body})if submitted then state.commandIds[#state.commandIds+1]=commandId else state.lastSendError=submitErr or\"could not reach CeetOS network service\"end state.nextAttemptAt=timestamp+AUTH_SEND_RETRY_MS submittedNow=true end local shouldRead=event==nil or submittedNow or event==\"ceetos_auth_reply\"and tostring(eventId or\"\")==tostring(state.id)if event==networkIpc.RESULT_EVENT then for _,commandId in ipairs(state.commandIds)do if tostring(eventId or\"\")==tostring(commandId)then shouldRead=true;break end end end if shouldRead then for index=#state.commandIds,1,-1 do local commandId=state.commandIds[index]local acknowledgement,pollErr=networkIpc.poll(commandId,true)if pollErr then return finishRequest(state,nil,pollErr)end if acknowledgement then table.remove(state.commandIds,index)if acknowledgement.ok~=true then state.lastSendError=acknowledgement.error or\"Auth Server request was not sent\"end end end local reply=take(state.id)if reply then delivery.pending=math.max(0,delivery.pending-1)return finishRequest(state,reply.ok and reply.result or nil,reply.error or\"Auth Server rejected request\")end local deferred=takeDeferred(state.id)if deferred then M.receive(deferred.body,deferred.context)reply=take(state.id)if reply then delivery.pending=math.max(0,delivery.pending-1)return finishRequest(state,reply.ok and reply.result or nil,reply.error or\"Auth Server rejected request\")end end end if timestamp>=state.deadline then return finishRequest(state,nil,state.lastSendError or\"Auth Server did not respond\")end return nil,nil end function M.startLogin(name,plainPassword)if type(name)~=\"string\"or type(plainPassword)~=\"string\"then return{done=true,error=\"invalid credentials\"}end local cloud=require(\"ceetos.lib.cloud_sync\")if cloud.authOnline()then return{done=true,error=\"cloud login requires the normal terminal path\"}end local request,err=startRequest(\"auth_login_begin\",{request_id=requestId(\"begin\"),username=name,node=tostring(os.getComputerID())},AUTH_BEGIN_TIMEOUT,{routedChallenge=true,node=tostring(os.getComputerID()),})if not request then return{done=true,error=err}end return{kind=\"login\",name=name,password=plainPassword,phase=\"begin\",request=request,beginAttempts=1}end function M.startProbe()local request,err=startRequest(\"auth_login_begin\",{request_id=requestId(\"begin\"),username=\"ceetos_probe\",node=tostring(os.getComputerID())},AUTH_BEGIN_TIMEOUT,{routedChallenge=true,node=tostring(os.getComputerID()),})if not request then return{kind=\"probe\",done=true,error=err}end return{kind=\"probe\",phase=\"begin\",request=request,beginAttempts=1}end function M.step(task,event,eventId)if type(task)~=\"table\"then return true,false,\"invalid authentication transaction\"end if task.done then return true,task.ok==true,task.error end local result,err=tickRequest(task.request,event,eventId)if not task.request.done then return false end if task.kind==\"probe\"then if err==\"invalid credentials\"then task.done,task.ok,task.result=true,true,{authority=task.request.server.id,request=task.request.id};return true,true end task.done,task.ok,task.error=true,false,err or\"Auth Server probe returned an unexpected response\"return true,false,task.error end if task.phase==\"begin\"then if not result then if err==\"Auth Server did not respond\"and task.beginAttempts<AUTH_BEGIN_ATTEMPTS then task.beginAttempts=task.beginAttempts+1 local request,startErr=startRequest(\"auth_login_begin\",{request_id=requestId(\"begin\"),username=task.name,node=tostring(os.getComputerID())},AUTH_BEGIN_TIMEOUT,{routedChallenge=true,node=tostring(os.getComputerID()),})if request then task.request=request;return false end task.done,task.ok,task.error=true,false,startErr;return true,false,startErr end task.done,task.ok,task.error=true,false,err or\"Auth Server did not respond\";return true,false,task.error end local verifier,deriveErr=password.derive(task.password,result.salt,result.workFactor)if not verifier then task.done,task.ok,task.error=true,false,deriveErr;return true,false,deriveErr end local proofId=requestId(\"proof\")local proof=loginProof(verifier,result.authority,task.request.id,result.challenge,result.expires)local request,startErr=startRequest(\"auth_login_proof\",{request_id=proofId,challenge_id=task.request.id,username=task.name,node=tostring(os.getComputerID()),challenge=result.challenge,proof=proof},nil,{routedLogin=true,verifier=verifier,node=tostring(os.getComputerID()),challengeId=task.request.id,challenge=result.challenge,},AUTH_SEND_ATTEMPTS)if not request then task.done,task.ok,task.error=true,false,startErr;return true,false,startErr end task.phase,task.request=\"proof\",request return false end if not result then task.done,task.ok,task.error=true,false,err or\"Auth Server rejected login\";return true,false,task.error end local auth=require((\"ceetos.lib.auth\"))local saved,saveErr=auth.saveCentralSession(result.session)if not saved then task.done,task.ok,task.error=true,false,saveErr;return true,false,saveErr end if type(result.directory)==\"table\"then auth.updateDirectory(result.directory,result.revision)end task.done,task.ok,task.result=true,true,true return true,true end loginProof=function(verifier,server,request,challenge,expires)return password.hmacSha256(verifier,table.concat({\"ceetos/auth/login/v1\",tostring(server),tostring(os.getComputerID()),tostring(request),tostring(challenge),tostring(expires)},\"|\"))end local function beginLogin(name)local challengeId,begin,beginErr for attempt=1,AUTH_BEGIN_ATTEMPTS do challengeId=requestId(\"begin\")begin,beginErr=send(\"auth_login_begin\",{request_id=challengeId,username=name,node=tostring(os.getComputerID())},AUTH_BEGIN_TIMEOUT,{routedChallenge=true,node=tostring(os.getComputerID()),})if begin or beginErr~=\"Auth Server did not respond\"then break end if attempt<AUTH_BEGIN_ATTEMPTS then sleep(0.1)end end return begin,beginErr,challengeId end function M.login(name,plainPassword)if type(name)~=\"string\"or type(plainPassword)~=\"string\"then return false,\"invalid credentials\"end local cloud=require(\"ceetos.lib.cloud_sync\")if cloud.authOnline()then return cloud.login(name,plainPassword)end local begin,beginErr,challengeId=beginLogin(name)if not begin then return false,beginErr end local verifier,deriveErr=password.derive(plainPassword,begin.salt,begin.workFactor)if not verifier then return false,deriveErr end local proof=loginProof(verifier,begin.authority,challengeId,begin.challenge,begin.expires)local proofId=requestId(\"proof\")local result,proofErr=send(\"auth_login_proof\",{request_id=proofId,challenge_id=challengeId,username=name,node=tostring(os.getComputerID()),challenge=begin.challenge,proof=proof},nil,{routedLogin=true,verifier=verifier,node=tostring(os.getComputerID()),challengeId=challengeId,challenge=begin.challenge,},AUTH_SEND_ATTEMPTS)if not result then return false,proofErr end local auth=require((\"ceetos.lib.auth\"))local saved,saveErr=auth.saveCentralSession(result.session)if not saved then return false,saveErr end if type(result.directory)==\"table\"then auth.updateDirectory(result.directory,result.revision)end return true end function M.probe()local server=authority()if not server then return false,\"no Auth Server is reachable\"end local _,err,probeId=beginLogin(\"ceetos_probe\")if err==\"invalid credentials\"then return true,{authority=server.id,request=probeId}end return false,err or\"Auth Server probe returned an unexpected response\"end function M.mutate(action,fields)local auth=require((\"ceetos.lib.auth\"))local cloud=require(\"ceetos.lib.cloud_sync\")if cloud.authOnline()then return cloud.mutate(action,fields)end local current,server=auth.refreshSession(),authority()if not current or not current.central or not server then return false,\"an Auth Server login is required\"end local result,err=send(\"auth_mutate\",{action=action,fields=fields,session=current.nonce,node=tostring(os.getComputerID()),revision=current.accountRevision})if not result then return false,err end if type(result.directory)==\"table\"then auth.updateDirectory(result.directory,result.revision)end return true end return M",
  ["ceetos/lib/jobs.lua"] = "local store=require(\"ceetos.lib.store\")local audit=require(\"ceetos.lib.audit\")local M={}local PATH,WORKERS_PATH=\"/ceetos/data/jobs.lua\",\"/ceetos/data/job-workers.lua\"local STATE_SCHEMA=2 local HISTORY_LIMIT,OUTPUT_LIMIT=48,4096 local HEARTBEAT_TIMEOUT,DEFAULT_CAPACITY=15000,1 local DEFAULT_TIMEOUT,DEFAULT_ATTEMPTS=60000,2 local context=nil local function now()if context and context.now then return context.now()end return os.epoch(\"utc\")end local function localId()if context and(context.id~=nil or context.nodeId~=nil)then return tostring(context.id or context.nodeId)end return tostring(os.getComputerID())end local function backend()return context and context.store or store end local function paths()return context and context.path or PATH,context and context.workersPath or WORKERS_PATH end local function log(action,detail)if not(context and context.noAudit~=false)then audit.log(\"system\",action,detail)end end local function copy(value,seen)if type(value)~=\"table\"then return value end seen=seen or{}if seen[value]then return seen[value]end local result={};seen[value]=result for key,item in pairs(value)do result[copy(key,seen)]=copy(item,seen)end return result end local function clampNumber(value,low,high,fallback)value=tonumber(value)if not value then return fallback end return math.max(low,math.min(high,math.floor(value)))end local function cleanOutput(value)value=tostring(value or\"\")if#value>OUTPUT_LIMIT then return value:sub(1,OUTPUT_LIMIT)..\"\\n[output truncated]\"end return value end local function defaultState()return{controller=(context and context.controllerId and tostring(context.controllerId))or localId(),capacity=DEFAULT_CAPACITY,workers={},templates={},queue={},running={},schedules={},history={},sequence=0,fair=0,}end local function normalise(data)data=type(data)==\"table\"and data or defaultState()local defaults=defaultState()for key,value in pairs(defaults)do if data[key]==nil then data[key]=value end end data.controller=tostring(data.controller or localId())data.capacity=clampNumber(data.capacity,1,32,DEFAULT_CAPACITY)data.workers,data.templates,data.queue=data.workers or{},data.templates or{},data.queue or{}data.running,data.schedules,data.history=data.running or{},data.schedules or{},data.history or{}data.sequence,data.fair=tonumber(data.sequence)or 0,tonumber(data.fair)or 0 return data end local function load()local mainPath,workersPath=paths()local disk=backend()if disk.invalidate then disk.invalidate(mainPath);disk.invalidate(workersPath)end local raw=copy(disk.read(mainPath,defaultState()))if type(raw)==\"table\"and raw.schema==STATE_SCHEMA and type(raw.data)==\"table\"then return normalise(copy(raw.data))end local data=normalise(raw)data.workers=copy(disk.read(workersPath,data.workers or{})or{})return normalise(data)end local function save(data)local mainPath=paths()local disk=backend()local previous=disk.read(mainPath,{})local revision=type(previous)==\"table\"and previous.schema==STATE_SCHEMA and tonumber(previous.revision)or 0 local committed={schema=STATE_SCHEMA,revision=revision+1,data=copy(data)}local ok,result,err=pcall(disk.write,mainPath,committed)assert(ok and result~=false,err or result or\"could not commit job state\")if disk.invalidate then disk.invalidate(mainPath)end return committed.revision end local function nextId(data,prefix)data.sequence=data.sequence+1 return string.format(\"%s-%s-%d\",prefix,tostring(now()),data.sequence)end local function findQueue(data,jobId)for index,job in ipairs(data.queue)do if job.id==jobId then return index,job end end end local function finish(data,job,status,result)data.running[job.id]=nil local worker=job.worker and data.workers[tostring(job.worker)]if worker then worker.inFlight=math.max(0,(tonumber(worker.inFlight)or 0)-1)end job.status,job.finished,job.result=status,now(),result or{}job.result.output=cleanOutput(job.result.output or job.result.stdout or\"\")data.history[#data.history+1]=job local limit=(context and tonumber(context.historyLimit))or HISTORY_LIMIT while#data.history>limit do table.remove(data.history,1)end end local function addJob(data,templateName,scheduleId)local template=data.templates[templateName]assert(template and template.enabled~=false,\"template unavailable\")local job={id=nextId(data,\"job\"),template=templateName,revision=template.revision,command=template.command,scheduleId=scheduleId,created=now(),status=\"queued\",attempts=0,retrySafe=template.retrySafe==true,maxAttempts=template.maxAttempts,}data.queue[#data.queue+1]=job return job end function M.state()return copy(load())end function M.setController(value)assert(value~=nil and tostring(value)~=\"\",\"controller id required\")local data=load();data.controller=tostring(value);save(data)log(\"jobs_controller\",data.controller)return data.controller end function M.controller()return load().controller end function M.isController()return M.controller()==localId()end local function requireController()assert(M.isController(),\"controller required\")end function M.setCapacity(value)local data=load();data.capacity=clampNumber(value,1,32,DEFAULT_CAPACITY);save(data)return data.capacity end function M.heartbeat(peer,status)assert(peer~=nil and tostring(peer)~=\"\",\"worker id required\")status=type(status)==\"table\"and status or{}local data,id=load(),tostring(peer)local old=data.workers[id]or{}data.workers[id]={id=id,label=status.label or old.label or id,capacity=clampNumber(status.capacity,1,32,old.capacity or DEFAULT_CAPACITY),inFlight=clampNumber(status.inFlight,0,32,old.inFlight or 0),queueDepth=clampNumber(status.queueDepth,0,1024,0),route=status.route or old.route,hops=clampNumber(status.hops or status.routeHops,0,32,old.hops or 99),available=status.available~=false,lastSeen=now(),lastError=status.lastError,}save(data)return copy(data.workers[id])end M.updateWorker=M.heartbeat function M.workers()local data,at,result=load(),now(),{}for id,worker in pairs(data.workers)do local row=copy(worker)row.healthy=row.available~=false and at-(row.lastSeen or 0)<=HEARTBEAT_TIMEOUT row.slots=math.max(0,(row.capacity or DEFAULT_CAPACITY)-(row.inFlight or 0))result[id]=row end return result end function M.addTemplate(name,command,options)requireController()if type(name)==\"table\"then options,command,name=name,name.command,name.id or name.name end assert(type(name)==\"string\"and name:match(\"^[%w_%-]+$\"),\"invalid template name\")assert(type(command)==\"string\"and#command>0 and#command<=512,\"invalid fixed command\")options=type(options)==\"table\"and options or{}local data=load()assert(not data.templates[name],\"template already exists\")data.templates[name]={name=name,command=command,enabled=options.enabled~=false,timeout=clampNumber(options.timeout,1000,600000,DEFAULT_TIMEOUT),retrySafe=options.retrySafe==true,maxAttempts=clampNumber(options.maxAttempts,1,5,DEFAULT_ATTEMPTS),revision=nextId(data,\"template\"),created=now(),}save(data);log(\"jobs_template_added\",name)return copy(data.templates[name])end function M.listTemplates()return copy(load().templates)end function M.setTemplateRetrySafe(name,enabled)requireController()local data=load();assert(data.templates[name],\"unknown template\")data.templates[name].retrySafe=enabled==true;save(data)return copy(data.templates[name])end function M.setTemplateEnabled(name,enabled)requireController()local data=load();assert(data.templates[name],\"unknown template\")data.templates[name].enabled=enabled~=false;save(data)return copy(data.templates[name])end function M.removeTemplate(name)requireController()local data=load();assert(data.templates[name],\"unknown template\")for _,job in ipairs(data.queue)do assert(job.template~=name,\"template has queued jobs\")end for _,job in pairs(data.running)do assert(job.template~=name,\"template has running jobs\")end for _,schedule in pairs(data.schedules)do assert(schedule.template~=name or not schedule.enabled,\"template has active schedule\")end data.templates[name]=nil;save(data);log(\"jobs_template_removed\",name)return true end function M.enqueue(templateName,scheduleId)requireController()local data,job=load(),nil job=addJob(data,templateName,scheduleId)save(data)return copy(job)end function M.selectWorker()local data,at,candidates=load(),now(),{}for id,worker in pairs(data.workers)do local healthy=worker.available~=false and at-(worker.lastSeen or 0)<=HEARTBEAT_TIMEOUT local capacity,inFlight=worker.capacity or DEFAULT_CAPACITY,worker.inFlight or 0 if healthy and inFlight<capacity then candidates[#candidates+1]={id=id,worker=worker,load=inFlight/capacity,hops=tonumber(worker.hops)or tonumber(worker.routeHops)or 99}end end table.sort(candidates,function(a,b)if a.load~=b.load then return a.load<b.load end if a.hops~=b.hops then return a.hops<b.hops end return a.id<b.id end)if#candidates==0 then return nil end local best,count=candidates[1],1 while candidates[count+1]and candidates[count+1].load==best.load and candidates[count+1].hops==best.hops do count=count+1 end local chosen=candidates[(data.fair%count)+1]return copy(chosen.worker)end function M.dispatchNext()local job,worker=M.nextDispatch(),M.selectWorker()if not job or not worker then return nil,job and\"no healthy worker\"or\"no queued job\"end local assigned=M.assign(job,worker.id)if context and type(context.execute)==\"function\"then local template=M.listTemplates()[assigned.template]local ok,first,second=pcall(context.execute,copy(worker),copy(template),copy(assigned))if not ok then return M.fail(assigned.id,first)end if first==false then return M.fail(assigned.id,second or\"worker rejected job\")end if type(first)==\"table\"then return M.complete(assigned.id,first)end end return assigned end function M.run(templateName,scheduleId)assert(scheduleId==nil or type(scheduleId)==\"string\",\"runtime job arguments are not allowed\")local job=M.enqueue(templateName,scheduleId)local dispatched,reason=M.dispatchNext()return dispatched or job,reason end function M.nextDispatch()local data=load()for _,job in ipairs(data.queue)do if job.status==\"queued\"then return copy(job)end end return nil end function M.assign(jobValue,workerValue)local data,jobId,workerId=load(),type(jobValue)==\"table\"and jobValue.id or jobValue,tostring(workerValue)local index,job=findQueue(data,jobId)assert(job and job.status==\"queued\",\"job is not queued\")local worker=data.workers[workerId]assert(worker,\"unknown worker\")assert(worker.available~=false and now()-(worker.lastSeen or 0)<=HEARTBEAT_TIMEOUT,\"worker unavailable\")assert((worker.inFlight or 0)<(worker.capacity or DEFAULT_CAPACITY),\"worker saturated\")table.remove(data.queue,index)job.status,job.worker,job.started=\"running\",workerId,now()job.attempts=(job.attempts or 0)+1 job.deadline=job.started+(data.templates[job.template].timeout or DEFAULT_TIMEOUT)data.running[job.id]=job;worker.inFlight=(worker.inFlight or 0)+1 data.fair=data.fair+1 save(data)return copy(job)end function M.complete(jobId,result,stdout,stderr)local data,job=load(),nil job=data.running[jobId];assert(job,\"unknown running job\")if result==false then return M.fail(jobId,stderr or stdout or\"worker failed\")end local payload if type(result)==\"table\"then payload=copy(result)elseif stdout~=nil or stderr~=nil then payload={ok=result~=false,stdout=tostring(stdout or\"\"),stderr=tostring(stderr or\"\"),output=tostring(stdout or\"\")}else payload={output=tostring(result or\"\")}end finish(data,job,\"complete\",payload)save(data)return copy(job)end function M.fail(jobId,reason)local data,job=load(),nil job=data.running[jobId];assert(job,\"unknown running job\")data.running[job.id]=nil local worker=job.worker and data.workers[tostring(job.worker)]if worker then worker.inFlight=math.max(0,(worker.inFlight or 0)-1)end if job.retrySafe and(job.attempts or 0)<(job.maxAttempts or DEFAULT_ATTEMPTS)then job.status,job.worker,job.deadline=\"queued\",nil,nil job.lastError,job.updated=tostring(reason or\"failed\"),now()data.queue[#data.queue+1]=job save(data)return copy(job),true end finish(data,job,\"failed\",{error=tostring(reason or\"failed\")})save(data)return copy(job),false end function M.schedule(templateName,interval)requireController()interval=tonumber(interval)assert(interval and interval>=1 and interval<=86400,\"interval must be 1..86400 seconds\")local data=load()assert(data.templates[templateName],\"unknown template\")local id=nextId(data,\"schedule\")data.schedules[id]={id=id,template=templateName,interval=interval*1000,enabled=true,nextRun=now()+interval*1000,created=now()}save(data);log(\"jobs_schedule_added\",id)return copy(data.schedules[id])end function M.cancel(scheduleId)requireController()local data=load();assert(data.schedules[scheduleId],\"unknown schedule\")data.schedules[scheduleId].enabled=false;data.schedules[scheduleId].cancelled=now();save(data)return true end function M.history()return copy(load().history)end function M.queue()return copy(load().queue)end function M.running()return copy(load().running)end function M.localStatus()local data=load()return{id=localId(),controller=data.controller,capacity=data.capacity,inFlight=(function()local total=0;for _ in pairs(data.running)do total=total+1 end;return total end)(),queueDepth=#data.queue}end function M.syncTemplates(controllerId,templates)assert(controllerId~=nil and tostring(controllerId)~=\"\",\"controller id required\")assert(type(templates)==\"table\",\"template snapshot required\")local data=load();data.controller,data.templates=tostring(controllerId),{}for name,template in pairs(templates)do assert(type(name)==\"string\"and type(template)==\"table\"and type(template.command)==\"string\",\"invalid template snapshot\")data.templates[name]={name=name,command=template.command,enabled=template.enabled~=false,timeout=clampNumber(template.timeout,1000,600000,DEFAULT_TIMEOUT),retrySafe=template.retrySafe==true,maxAttempts=clampNumber(template.maxAttempts,1,5,DEFAULT_ATTEMPTS),revision=template.revision or\"synced\",created=template.created or now(),}end save(data)return copy(data.templates)end M.scheduleInterval=M.schedule function M.tick()local data,at,added,timedOut,stale=load(),now(),0,0,0 for _,schedule in pairs(data.schedules)do if M.isController()and schedule.enabled and at>=(schedule.nextRun or at)then addJob(data,schedule.template,schedule.id);added=added+1 repeat schedule.nextRun=(schedule.nextRun or at)+schedule.interval until schedule.nextRun>at end end for _,worker in pairs(data.workers)do if worker.available~=false and at-(worker.lastSeen or 0)>HEARTBEAT_TIMEOUT then worker.stale=true;stale=stale+1 end end local expired={}for id,job in pairs(data.running)do if at>=(job.deadline or at)then expired[#expired+1]=id end end for _,id in ipairs(expired)do local job=data.running[id]if job then timedOut=timedOut+1;data.running[id]=nil local worker=job.worker and data.workers[tostring(job.worker)]if worker then worker.inFlight=math.max(0,(worker.inFlight or 0)-1)end if job.retrySafe and(job.attempts or 0)<(job.maxAttempts or DEFAULT_ATTEMPTS)then job.status,job.worker,job.deadline,job.lastError=\"queued\",nil,nil,\"timeout\";data.queue[#data.queue+1]=job else finish(data,job,\"failed\",{error=\"timeout\"})end end end if added>0 or timedOut>0 or stale>0 then save(data)end if context and type(context.execute)==\"function\"then M.dispatchNext()end return{enqueued=added,timedOut=timedOut,staleWorkers=stale}end function M.new(options)options=options or{}local service={}local names={\"state\",\"setController\",\"controller\",\"isController\",\"setCapacity\",\"heartbeat\",\"updateWorker\",\"workers\",\"addTemplate\",\"listTemplates\",\"setTemplateRetrySafe\",\"setTemplateEnabled\",\"removeTemplate\",\"syncTemplates\",\"enqueue\",\"run\",\"nextDispatch\",\"selectWorker\",\"dispatchNext\",\"assign\",\"complete\",\"fail\",\"tick\",\"schedule\",\"scheduleInterval\",\"cancel\",\"history\",\"queue\",\"running\",\"localStatus\",}local function invoke(fn,...)local prior=context;context=options local result={pcall(fn,...)}context=prior if not result[1]then error(result[2],0)end return table.unpack(result,2)end for _,name in ipairs(names)do service[name]=function(...)local args={...}if args[1]==service then table.remove(args,1)end return invoke(M[name],table.unpack(args))end end return service end return M",
  ["ceetos/lib/peripherals.lua"] = "local store=require(\"ceetos.lib.store\")local audit=require(\"ceetos.lib.audit\")local M={}local PATH=\"/ceetos/data/shares.lua\"local SAFE={getItemDetail=true,list=true,size=true,getMetadata=true,getType=true,getMethods=true,getInput=true,getOutput=true,getAngles=true,getVelocity=true,getHeight=true,getAirPressure=true,getSpeed=true,getStress=true,getStressCapacity=true,stock=true,getStockItemDetail=true,getConfiguration=true,getAddress=true,}local function requestMatchesGrid(request,slots)if type(request)~=\"table\"then return false,\"redstone requester did not return its encoded request\"end for slot=1,9 do local expected=slots[slot]local actual=request[slot]or request[tostring(slot)]local actualName=type(actual)==\"table\"and actual.name or nil if expected==\"minecraft:air\"then if actualName and actualName~=\"minecraft:air\"then return false,\"redstone requester changed the exact 3x3 craft shape\"end elseif actualName~=expected then return false,\"redstone requester did not retain the exact 3x3 craft shape\"end end return true end local function shares()return store.read(PATH,{})end local function save(s)store.write(PATH,s)end local function safeMethods(name)local out={}local available=peripheral.getMethods and peripheral.getMethods(name)or nil if available then for _,method in ipairs(available)do if SAFE[method]then out[#out+1]=method end end else for method in pairs(SAFE)do out[#out+1]=method end end table.sort(out)return out end function M.listLocal()local result={}for _,name in ipairs(peripheral.getNames())do result[#result+1]={name=name,type=peripheral.getType(name)}end return result end function M.share(name,mode)assert(peripheral.isPresent(name),\"peripheral is not present\")assert(mode==\"peer\"or mode==\"master\",\"mode must be peer or master\")local s=shares()s[name]={mode=mode,methods=SAFE}save(s)audit.log(\"local\",\"peripheral.share\",{name=name,mode=mode})end function M.unshare(name)local s=shares();s[name]=nil;save(s)audit.log(\"local\",\"peripheral.unshare\",{name=name})end function M.shareAll(mode)assert(mode==\"peer\"or mode==\"master\",\"mode must be peer or master\")local count=0 for _,item in ipairs(M.listLocal())do M.share(item.name,mode);count=count+1 end audit.log(\"local\",\"peripheral.share_all\",{mode=mode,count=count})return count end function M.unshareAll()local s,count=shares(),0 for name in pairs(s)do s[name],count=nil,count+1 end save(s)audit.log(\"local\",\"peripheral.unshare_all\",{count=count})return count end function M.describe()local s,result=shares(),{}for name,spec in pairs(s)do if peripheral.isPresent(name)then result[#result+1]={name=name,type=peripheral.getType(name),mode=spec.mode,methods=safeMethods(name)}end end return result end function M.call(peerIsMaster,name,method,args)local spec=shares()[name]if not spec or not peripheral.isPresent(name)then return false,\"not shared\"end if spec.mode==\"master\"and not peerIsMaster then return false,\"master-only peripheral\"end if not spec.methods[method]then return false,\"method not allowed\"end local api=peripheral.wrap(name)if type(api[method])~=\"function\"then return false,\"method unavailable\"end local ok,result=pcall(api[method],table.unpack(args or{}))return ok,result end function M.craftRequest(peerIsMaster,name,address,filters)local spec=shares()[name]local localAccess=peerIsMaster==\"local\"if not peripheral.isPresent(name)then return false,\"stock ticker is unavailable\"end if not localAccess and not spec then return false,\"stock ticker is not shared\"end if not localAccess and spec.mode==\"master\"and not peerIsMaster then return false,\"master-only peripheral\"end if type(address)~=\"string\"or#address<1 or#address>64 or address:find(\"[%z\\1-\\31\\127]\")then return false,\"invalid destination address\"end if type(filters)~=\"table\"or#filters<1 or#filters>32 then return false,\"invalid craft filters\"end local clean={}for index,filter in ipairs(filters)do if type(filter)~=\"table\"or type(filter.name)~=\"string\"or#filter.name>128 then return false,\"invalid item filter \"..tostring(index)end local count=tonumber(filter._requestCount or filter.count)if not count or count<1 or count>256 or count~=math.floor(count)then return false,\"invalid request count\"end clean[index]={name=filter.name,_requestCount=count}end local api=peripheral.wrap(name)if not api or type(api.requestFiltered)~=\"function\"then return false,\"stock ticker requestFiltered is unavailable\"end local ok,result=pcall(api.requestFiltered,address,table.unpack(clean))if ok then audit.log(\"network\",\"peripheral.craft_request\",{name=name,address=address,filters=#clean})end return ok,result end function M.craftingRecipeRequest(peerIsMaster,name,address,batches,grid)local spec=shares()[name]local localAccess=peerIsMaster==\"local\"if not peripheral.isPresent(name)then return false,\"redstone requester is unavailable\"end if not localAccess and not spec then return false,\"redstone requester is not shared\"end if not localAccess and spec.mode==\"master\"and not peerIsMaster then return false,\"master-only peripheral\"end if type(address)~=\"string\"or#address<1 or#address>64 or address:find(\"[%z\\1-\\31\\127]\")then return false,\"invalid craft address\"end batches=tonumber(batches)if not batches or batches<1 or batches>256 or batches~=math.floor(batches)then return false,\"invalid craft batches\"end if type(grid)~=\"table\"then return false,\"invalid crafting grid\"end local slots={}for slot=1,9 do local value=grid[slot]if value~=nil and value~=false and(type(value)~=\"string\"or#value>128)then return false,\"invalid craft grid slot\"end if type(value)==\"string\"and value:sub(1,1)==\"#\"then return false,\"Create Redstone Requester requires concrete item IDs; resolve recipe tags first\"end slots[slot]=(value==nil or value==false)and\"minecraft:air\"or value end local api=peripheral.wrap(name)if not api or type(api.setCraftingRequest)~=\"function\"or type(api.setAddress)~=\"function\"or type(api.request)~=\"function\"then return false,\"redstone requester crafting API is unavailable\"end local ok,result=pcall(function()if type(api.setConfiguration)==\"function\"then api.setConfiguration(\"strict\")end api.setCraftingRequest(batches,slots[1],slots[2],slots[3],slots[4],slots[5],slots[6],slots[7],slots[8],slots[9])api.setAddress(address)local configured=nil if type(api.getRequest)==\"function\"then configured=api.getRequest()local retained,retainError=requestMatchesGrid(configured,slots)if not retained then error(retainError)end end if type(api.getAddress)==\"function\"and api.getAddress()~=address then error(\"redstone requester did not retain the craft address\")end local submitted=api.request()if submitted==false then error(\"redstone requester rejected the crafting request\")end return{submitted=true,strict=type(api.setConfiguration)==\"function\",configured=configured~=nil,address=address}end)if ok then audit.log(\"network\",\"peripheral.crafting_recipe_request\",{name=name,address=address,batches=batches})end return ok,result end return M",
  ["ceetos/lib/telemetry.lua"] = "local store=require(\"ceetos.lib.store\")local M={}local PATH=\"/ceetos/data/config.lua\"local activity=\"desktop\"local function text(value,limit)if type(value)~=\"string\"then return nil end value=value:gsub(\"[%z\\1-\\31\\127]\",\" \")if value==\"\"then return nil end return value:sub(1,limit)end local function config()local value=store.read(PATH,{})if type(value)~=\"table\"then value={}end if value.telemetry==nil then value.telemetry=true store.write(PATH,value)end return value end function M.setEnabled(value)assert(type(value)==\"boolean\",\"telemetry state must be boolean\")require(\"ceetos.lib.auth\").require(\"admin\")local valueForSave=config()valueForSave.telemetry=value return store.write(PATH,valueForSave)end function M.status()return config().telemetry==true end function M.configuration()return{enabled=M.status()}end function M.markActivity(value)activity=text(value,48)or activity return activity end function M.activity()return activity end function M.snapshot(currentActivity)if currentActivity then M.markActivity(currentActivity)end local data={ts=os.epoch(\"utc\"),id=os.getComputerID(),label=text(os.getComputerLabel(),96),activity=activity,}if turtle and type(turtle.getFuelLevel)==\"function\"then local ok,fuel=pcall(turtle.getFuelLevel)if ok and type(fuel)==\"number\"then data.fuel=fuel end end if gps and type(gps.locate)==\"function\"then local ok,x,y,z=pcall(gps.locate,0.2)if ok and type(x)==\"number\"and type(y)==\"number\"and type(z)==\"number\"then data.gps={x=x,y=y,z=z}end end return data end return M",
  ["ceetos/lib/cloud.lua"] = "local store=require(\"ceetos.lib.store\")local password=require(\"ceetos.lib.password\")local profile=require(\"ceetos.lib.profile\")local M={}M.PATH=\"/ceetos/data/cloud.lua\"M.MAX_BODY=48*1024 M.MAX_RESPONSE=64*1024 M.DEFAULTS={auth=\"https://auth.ceet.uk\",recipe=\"https://recipe.ceet.uk\",dashboard=\"https://dash.ceet.uk\",}local function now()return os.epoch(\"utc\")end local function validUrl(value)if type(value)~=\"string\"or#value>160 then return false end return value:match(\"^https://[%w%-%.]+%.ceet%.uk$\")~=nil end local function validId(value)return type(value)==\"string\"and value:match(\"^[%w_.%-]+$\")~=nil and#value<=96 end local function copy(value)local out={}for key,item in pairs(value or{})do out[key]=item end return out end local function normalise(value)value=type(value)==\"table\"and value or{}local urls=type(value.urls)==\"table\"and value.urls or{}local result={schema=1,enabled=value.enabled==true,urls={auth=validUrl(urls.auth)and urls.auth or M.DEFAULTS.auth,recipe=validUrl(urls.recipe)and urls.recipe or M.DEFAULTS.recipe,dashboard=validUrl(urls.dashboard)and urls.dashboard or M.DEFAULTS.dashboard,},node=type(value.node)==\"table\"and copy(value.node)or nil,}if result.node and(not validId(result.node.id)or type(result.node.secret)~=\"string\"or#result.node.secret<32 or#result.node.secret>192)then result.node=nil end return result end function M.load()store.invalidate(M.PATH)return normalise(store.read(M.PATH,{}))end function M.save(value)local clean=normalise(value)return store.write(M.PATH,clean)end function M.configure(urls)local value=M.load()urls=type(urls)==\"table\"and urls or{}for key in pairs(M.DEFAULTS)do if urls[key]~=nil then if not validUrl(urls[key])then return false,\"invalid \"..key..\" cloud URL\"end value.urls[key]=urls[key]end end return M.save(value)end function M.clear()local value=M.load();value.enabled,value.node=false,nil return M.save(value)end function M.publicStatus()local value=M.load()return{enabled=value.enabled,enrolled=value.node~=nil,node=value.node and value.node.id or nil,urls=copy(value.urls),profile=profile.id()}end local function readBounded(handle,limit)local chunks,total={},0 while true do local part=handle.read(math.min(4096,limit-total+1))if not part or part==\"\"then break end total=total+#part if total>limit then return nil,\"cloud response is too large\"end chunks[#chunks+1]=part end return table.concat(chunks)end local function json(value)local ok,result=pcall(textutils.unserialiseJSON,value)return ok and type(result)==\"table\"and result or nil end local function canonical(node,timestamp,nonce,method,path,body)local digest=assert(password.sha256(body))return table.concat({\"ceetos-cloud-node-v1\",node,tostring(timestamp),nonce,method,path,digest},\"\\n\")end local nonceCounter=0 local function nonce()nonceCounter=nonceCounter+1 return password.newSalt(\"cloud:\"..tostring(nonceCounter))end local function perform(url,method,path,body,headers)if not http or type(http.post)~=\"function\"then return nil,\"HTTP is unavailable\"end local response local ok,err=pcall(function()response=http.post(url..path,body,headers)end)if not ok or not response then return nil,tostring(err or\"cloud request failed\")end local raw,readErr=readBounded(response,M.MAX_RESPONSE)pcall(response.close)if not raw then return nil,readErr end local decoded=json(raw)if not decoded then return nil,\"cloud returned invalid JSON\"end if decoded.ok==false then return nil,tostring(decoded.error or\"cloud rejected request\"):sub(1,180)end return decoded end function M.enroll(code)if type(code)~=\"string\"or#code<12 or#code>128 or code:find(\"[%z\\1-\\31\\127]\")then return nil,\"invalid enrollment code\"end local value=M.load()local payload=textutils.serialiseJSON({code=code,profile=profile.id(),label=os.getComputerLabel()or(\"Computer \"..tostring(os.getComputerID())),computerId=tostring(os.getComputerID())})if#payload>M.MAX_BODY then return nil,\"enrollment request is too large\"end local result,err=perform(value.urls.auth,\"POST\",\"/v1/nodes/enroll\",payload,{[\"Content-Type\"]=\"application/json\"})if not result then return nil,err end if type(result.node)~=\"table\"or not validId(result.node.id)or type(result.node.secret)~=\"string\"or#result.node.secret<32 then return nil,\"cloud returned invalid enrollment\"end value.enabled,value.node=true,{id=result.node.id,secret=result.node.secret,enrolledAt=now()}local saved,saveErr=M.save(value)if not saved then return nil,saveErr or\"could not save cloud enrollment\"end return M.publicStatus()end function M.call(service,path,payload)local value=M.load()if not value.enabled or not value.node then return nil,\"cloud node is not enrolled\"end if not M.DEFAULTS[service]or type(path)~=\"string\"or not path:match(\"^/v1/[%w%-%._/]+$\")or#path>128 then return nil,\"invalid cloud endpoint\"end local body=textutils.serialiseJSON(type(payload)==\"table\"and payload or{})if#body>M.MAX_BODY then return nil,\"cloud request is too large\"end local timestamp,requestNonce=now(),nonce()local signature=password.hmacSha256(value.node.secret,canonical(value.node.id,timestamp,requestNonce,\"POST\",path,body))if not signature then return nil,\"could not sign cloud request\"end return perform(value.urls[service],\"POST\",path,body,{[\"Content-Type\"]=\"application/json\",[\"X-CeetOS-Node\"]=value.node.id,[\"X-CeetOS-Time\"]=tostring(timestamp),[\"X-CeetOS-Nonce\"]=requestNonce,[\"X-CeetOS-Signature\"]=signature,})end return M",
  ["ceetos/lib/cloud_sync.lua"] = "local cloud=require(\"ceetos.lib.cloud\")local store=require(\"ceetos.lib.store\")local password=require(\"ceetos.lib.password\")local profile=require(\"ceetos.lib.profile\")local telemetryService=require(\"ceetos.lib.telemetry\")local RECIPE_SERVICE_MODULE=\"ceetos.lib.\"..\"recipe_service\"local M={}local STATE=\"/ceetos/data/cloud-state.lua\"local INTERVALS={telemetry=24*60*60*1000,authority=30000,recipes=30000}local TELEMETRY_RETRIES={15*60*1000,60*60*1000,4*60*60*1000}local function now()return os.epoch(\"utc\")end local function defaults()return{schema=2,revision=0,auth={online=false,next=0},recipe={online=false,next=0,revision=0},telemetry={enabled=nil,next=0,lastSuccess=nil,failures=0,lastError=nil},lastError=nil}end local function state()store.invalidate(STATE)local value=store.read(STATE,defaults())if type(value)~=\"table\"then value=defaults()end if type(value.auth)~=\"table\"then value.auth={}end if type(value.recipe)~=\"table\"then value.recipe={}end if type(value.telemetry)~=\"table\"then value.telemetry={}end for key,fallback in pairs(defaults())do if value[key]==nil then value[key]=fallback end end for key,fallback in pairs(defaults().recipe)do if value.recipe[key]==nil then value.recipe[key]=fallback end end for key,fallback in pairs(defaults().telemetry)do if value.telemetry[key]==nil then value.telemetry[key]=fallback end end return value end local function save(value)value.revision=(tonumber(value.revision)or 0)+1 return store.write(STATE,value)end local function errorState(value,message)value.lastError=tostring(message or\"cloud request failed\"):sub(1,180)end function M.status()local value,cfg=state(),cloud.publicStatus()return{enabled=cfg.enabled,enrolled=cfg.enrolled,node=cfg.node,urls=cfg.urls,authOnline=value.auth.online==true,recipeOnline=value.recipe.online==true,telemetryNext=tonumber(value.telemetry and value.telemetry.next)or 0,telemetryInterval=INTERVALS.telemetry,telemetryEnabled=telemetryService.status(),telemetryLastSuccess=value.telemetry and value.telemetry.lastSuccess or nil,telemetryLastError=value.telemetry and value.telemetry.lastError or nil,telemetryFailures=math.max(0,math.floor(tonumber(value.telemetry and value.telemetry.failures)or 0)),lastSync=value.lastSync,lastError=value.lastError,}end function M.configure(urls)return cloud.configure(urls)end function M.enroll(code)local enrolled,err=cloud.enroll(code)if not enrolled then return nil,err end local value=state()value.telemetry.next,value.telemetry.failures,value.telemetry.lastError=0,0,nil save(value)return enrolled end function M.clear()local ok,err=cloud.clear();if ok then store.write(STATE,defaults())end return ok,err end function M.authOnline()return M.status().authOnline==true end function M.recipeOnline()return M.status().recipeOnline==true end local function cloudAuthority(result)local auth=require(\"ceetos.lib.auth\")if type(result)~=\"table\"or type(result.directory)~=\"table\"then return false,\"invalid cloud authority response\"end local cfg=cloud.publicStatus()local endpoint=type(cfg.urls)==\"table\"and cfg.urls.auth or nil if type(endpoint)~=\"string\"then return false,\"cloud auth endpoint is unavailable\"end local record={id=\"cloud:\"..endpoint,term=tonumber(result.term)or 0,revision=tonumber(result.revision)or 0}local ok,accepted,reason=pcall(auth.acceptCloudAuthority,record,endpoint)if not ok then return false,accepted end if not accepted then return false,reason or\"cloud authority was rejected\"end local directoryOk,directoryErr=auth.updateDirectory(result.directory,record.revision)if not directoryOk then return false,directoryErr end return true end function M.login(name,plainPassword)if type(name)~=\"string\"or type(plainPassword)~=\"string\"then return false,\"invalid credentials\"end local begin,beginErr=cloud.call(\"auth\",\"/v1/auth/login/begin\",{username=name,node=tostring(os.getComputerID())})if not begin then return false,beginErr end if type(begin.salt)~=\"string\"or type(begin.workFactor)~=\"number\"or type(begin.challenge)~=\"string\"or type(begin.expires)~=\"number\"then return false,\"invalid cloud login challenge\"end local verifier,deriveErr=password.derive(plainPassword,begin.salt,begin.workFactor)if not verifier then return false,deriveErr end local proof=password.hmacSha256(verifier,table.concat({\"ceetos/auth/login/v1\",tostring(begin.authority),tostring(os.getComputerID()),tostring(begin.requestId),tostring(begin.challenge),tostring(begin.expires)},\"|\"))local result,resultErr=cloud.call(\"auth\",\"/v1/auth/login/proof\",{username=name,node=tostring(os.getComputerID()),requestId=begin.requestId,challenge=begin.challenge,proof=proof})if not result then return false,resultErr end local auth=require(\"ceetos.lib.auth\")local saved,saveErr=auth.saveCentralSession(result.session)if not saved then return false,saveErr end if type(result.directory)==\"table\"then auth.updateDirectory(result.directory,result.revision)end return true end function M.mutate(action,fields)local auth=require(\"ceetos.lib.auth\")local current=auth.refreshSession()if not current or not current.central then return false,\"cloud login is required\"end local result,err=cloud.call(\"auth\",\"/v1/auth/mutate\",{action=action,fields=fields,session=current.nonce,node=tostring(os.getComputerID()),revision=current.accountRevision})if not result then return false,err end if type(result.directory)==\"table\"then auth.updateDirectory(result.directory,result.revision)end return true end function M.recipePlan(item,quantity,stock)if not M.recipeOnline()then return nil,\"cloud Recipe Server is unavailable\"end local result,err=cloud.call(\"recipe\",\"/v1/recipe/plan\",{item=item,quantity=quantity,stock=stock})if not result then return nil,err end return result.plan,result.error end local function telemetry()local snapshot=telemetryService.snapshot()local net=require(\"ceetos.lib.net\")local transport=net.statusSnapshot and net.statusSnapshot().transport or{}snapshot.health={profile=profile.id(),version=require(\"ceetos.lib.version\").string,uptime=os.clock(),freeSpace=fs.getFreeSpace(\"/\"),peers=#(net.peers and net.peers()or{}),healthyLinks=transport and transport.healthyLinks or 0,lastError=M.status().lastError,}local result,err=cloud.call(\"dashboard\",\"/v1/telemetry/ingest\",snapshot)return result~=nil,err end local function syncAuth()if profile.is(\"auth-server\")then local auth=require(\"ceetos.lib.auth\")local snapshot=auth.authoritySnapshot()local pushed,pushErr=cloud.call(\"auth\",\"/v1/auth/replica/merge\",{snapshot=snapshot,source=\"mesh\"})if not pushed then return false,pushErr end if type(pushed.snapshot)==\"table\"then auth.merge(pushed.snapshot,true)end local ok,authErr=cloudAuthority(pushed)if not ok then return false,authErr end return true end local result,err=cloud.call(\"auth\",\"/v1/auth/directory\",{})if not result then return false,err end local ok,authErr=cloudAuthority(result)if not ok then return false,authErr end return true end local function syncRecipes()if not profile.is(\"recipe-server\")then local result,err=cloud.call(\"recipe\",\"/v1/recipe/status\",{})return result~=nil,err end local service=require(RECIPE_SERVICE_MODULE)if type(service.exportOverlay)~=\"function\"then return false,\"recipe export unavailable\"end local payload,exportErr=service.exportOverlay()if not payload then return false,exportErr end local value=state()value.recipe=value.recipe or{revision=0}local result,err=cloud.call(\"recipe\",\"/v1/recipe/replica/merge\",{overlay=payload,baseRevision=math.max(0,math.floor(tonumber(value.recipe.revision)or 0)),})if not result then return false,err end if type(result.overlay)==\"table\"and type(service.replaceOverlay)==\"function\"then local applied,applyErr=service.replaceOverlay(result.overlay)if not applied then return false,applyErr end end return true,nil,math.max(0,math.floor(tonumber(result.revision)or 0))end function M.tick(activity,timestampOverride)local cfg=cloud.publicStatus();if not cfg.enabled or not cfg.enrolled then return false,\"cloud node is not enrolled\"end local value,timestamp=state(),tonumber(timestampOverride)or now()local changed=false if timestamp>=(tonumber(value.auth.next)or 0)then local ok,err=syncAuth();value.auth.online,value.auth.next=ok==true,timestamp+INTERVALS.authority if not ok then errorState(value,err)else value.lastError=nil end changed=true end if timestamp>=(tonumber(value.recipe.next)or 0)then local ok,err,recipeRevision=syncRecipes();value.recipe.online,value.recipe.next=ok==true,timestamp+INTERVALS.recipes if recipeRevision~=nil then value.recipe.revision=recipeRevision end if not ok then errorState(value,err)end changed=true end local telemetryEnabled=telemetryService.status()if not telemetryEnabled then if value.telemetry.enabled~=false or value.telemetry.next~=0 then value.telemetry.enabled,value.telemetry.next,value.telemetry.failures,value.telemetry.lastError=false,0,0,nil changed=true end elseif value.telemetry.enabled~=true then value.telemetry.enabled,value.telemetry.next,value.telemetry.failures,value.telemetry.lastError=true,0,0,nil changed=true elseif timestamp>=(tonumber(value.telemetry.next)or 0)then local ok,err=telemetry()if ok then value.telemetry.next,value.telemetry.lastSuccess,value.telemetry.failures,value.telemetry.lastError=timestamp+INTERVALS.telemetry,timestamp,0,nil else local failures=math.max(0,math.floor(tonumber(value.telemetry.failures)or 0))+1 value.telemetry.failures,value.telemetry.lastError=failures,tostring(err or\"telemetry upload failed\"):sub(1,180)value.telemetry.next=timestamp+(TELEMETRY_RETRIES[failures]or INTERVALS.telemetry)errorState(value,value.telemetry.lastError)end changed=true end if changed then value.lastSync=timestamp;save(value)end return true end return M",
  ["ceetos/lib/cloud_service.lua"] = "local M={}M.EVENT=\"ceetos_cloud_sync\"local Service={}Service.__index=Service local function delay(value)value=tonumber(value)or 5 return math.max(0.5,math.min(value,60))end function M.new(options)options=options or{}assert(type(options.tick)==\"function\",\"cloud service requires tick\")assert(type(options.startTimer)==\"function\",\"cloud service requires startTimer\")return setmetatable({tick=options.tick,startTimer=options.startTimer,pullEvent=options.pullEvent,interval=delay(options.interval),timer=nil,runs=0,lastManual=false,},Service)end function Service:start()if not self.timer then self.timer=self.startTimer(self.interval)end end function Service:step(event)assert(type(event)==\"table\",\"cloud service event must be a table\")self:start()local manual=event[1]==M.EVENT if not manual and not(event[1]==\"timer\"and event[2]==self.timer)then return false end self.lastManual=manual self.tick(manual and\"manual\"or\"scheduled\")self.runs=self.runs+1 self.timer=self.startTimer(self.interval)return true end function Service:run()assert(type(self.pullEvent)==\"function\",\"cloud service requires pullEvent\")self:start()while true do self:step({self.pullEvent()})end end return M",
  ["ceetos/lib/owner_service.lua"] = "local store=require(\"ceetos.lib.store\")local password=require(\"ceetos.lib.password\")local telemetry=require(\"ceetos.lib.telemetry\")local M={}local PATH=\"/ceetos/data/owner-service.lua\"M.VERSION=1 M.MAX_AGE_MS=60*1000 M.MAX_USED_NONCES=64 local actions={snapshot=true,status=true}local function now()return os.epoch(\"utc\")end local function state()local value=store.read(PATH,{})if type(value)~=\"table\"then value={}end if value.enabled==nil then value.enabled=true end value.version=M.VERSION if type(value.used)~=\"table\"then value.used={}end return value end local function save(value)return store.write(PATH,value)end local function validToken(token)return type(token)==\"string\"and#token>=32 and#token<=128 and not token:find(\"[%z\\1-\\31\\127]\")end local function validId(value)return type(value)==\"string\"and value:match(\"^[%w_%-]+$\")and#value>=8 and#value<=96 end local function validNonce(value)return type(value)==\"string\"and value:match(\"^[%x]+$\")and#value>=16 and#value<=128 end local function canonical(node,request)return table.concat({\"ceetos-owner-service-v1\",tostring(node),tostring(request.id),tostring(request.action),tostring(math.floor(tonumber(request.ts)or-1)),tostring(request.nonce),},\"\\n\")end local function prune(value,time)local kept,rows={},{}for nonce,expires in pairs(value.used or{})do expires=tonumber(expires)if validNonce(nonce)and expires and expires>time then rows[#rows+1]={nonce=nonce,expires=expires}end end table.sort(rows,function(a,b)return a.expires>b.expires end)for index,row in ipairs(rows)do if index<=M.MAX_USED_NONCES then kept[row.nonce]=row.expires end end value.used=kept end function M.status()local value=state()prune(value,now())return{enabled=value.enabled~=false,provisioned=validToken(value.token),owner=type(value.owner)==\"string\"and value.owner or nil,actions={\"snapshot\",\"status\"},}end function M.setEnabled(enabled)local value=state();value.enabled=enabled==true;prune(value,now());save(value)return M.status()end function M.provision(token,owner)assert(validToken(token),\"owner-service token must be 32-128 printable characters\")assert(type(owner)==\"string\"and owner:match(\"^[%w_%-]+$\")and#owner<=32,\"invalid owner\")local value=state()value.token,value.owner,value.enabled,value.used=token,owner,true,{}assert(save(value),\"could not save owner-service configuration\")return M.status()end function M.rotate(owner)local token=password.newSalt(\"owner-service-a\")..password.newSalt(\"owner-service-b\")M.provision(token,owner)return token end function M.signature(token,node,request)assert(validToken(token),\"invalid owner-service token\")return assert(password.hmacSha256(token,canonical(node,request)))end function M.verify(request)if type(request)~=\"table\"then return false,\"service request must be an object\"end local value,time=state(),now()prune(value,time)if value.enabled==false then return false,\"owner service is disabled\"end if not validToken(value.token)then return false,\"owner service has not been provisioned\"end if not validId(request.id)or not actions[request.action]or not validNonce(request.nonce)then return false,\"invalid service request\"end local timestamp=tonumber(request.ts)if not timestamp or math.floor(timestamp)~=timestamp or math.abs(time-timestamp)>M.MAX_AGE_MS then return false,\"service request expired\"end if value.used[request.nonce]then return false,\"service request was already used\"end if type(request.tag)~=\"string\"or not request.tag:match(\"^[%x]+$\")or#request.tag~=64 then return false,\"invalid service authenticator\"end local expected=M.signature(value.token,os.getComputerID(),request)if not password.constantTimeEqual(expected,request.tag:lower())then return false,\"service authentication failed\"end value.used[request.nonce]=timestamp+M.MAX_AGE_MS prune(value,time)assert(save(value),\"could not persist service replay state\")return true,value end function M.dispatch(request)local ok,value=M.verify(request)if not ok then return false,value end if request.action==\"snapshot\"then return true,{action=\"snapshot\",telemetry=telemetry.snapshot(\"owner-service\"),owner=value.owner}end return true,{action=\"status\",service=M.status(),telemetryEnabled=telemetry.status()}end return M",
  ["ceetos/lib/gui.lua"] = "local M={}local store=require(\"ceetos.lib.store\")local terminalUi=require(\"ceetos.lib.terminal_ui\")M.pages={home=\"Desktop\",peers=\"Peers\",peripherals=\"Devices\",telemetry=\"Telemetry\",updates=\"Updates\",admin=\"Administration\",audit=\"Audit log\",jobs=\"Jobs\",files=\"Explorer\"}local function add(items,allowed,item)if allowed then items[#items+1]=item end end local function role(deps,required)return deps.auth.allowed(required)end local function accessNote(allowed,message)if allowed then return nil end return message end local function colour(item)if item.kind==\"directory\"then return colors.lightBlue end if item.kind==\"program\"then return colors.lime end if item.kind==\"file\"then return colors.white end return colors.lightGray end local function system(id,label)return{id=id,label=label,kind=\"system\",action=id}end local function panel(title,first,second)local lines={}if first and first~=\"\"then lines[#lines+1]=tostring(first)end if second and second~=\"\"then lines[#lines+1]=tostring(second)end return{title=tostring(title or\"Status\"),lines=lines}end local function grouped(item,group)item.group=group return item end local function count(value)if type(value)~=\"table\"then return 0 end local total=0 for _ in pairs(value)do total=total+1 end return total end local function entries(value)if type(value)~=\"table\"then return{}end local out={}for key,item in pairs(value)do if type(item)==\"table\"then local copy={}for field,fieldValue in pairs(item)do copy[field]=fieldValue end copy._ceetosKey=tostring(key)item=copy else item={value=item,_ceetosKey=tostring(key)}end out[#out+1]=item end table.sort(out,function(left,right)return tostring(left.name or left.id or left._ceetosKey)<tostring(right.name or right.id or right._ceetosKey)end)return out end local function short(value,limit)value=terminalUi.clean(value or\"-\")return#value>limit and value:sub(1,math.max(1,limit-3))..\"...\"or value end local function updateSnapshotKey(update)if type(update)~=\"table\"then return\"missing\"end local transfer,cache=update.transfer or{},update.cache or{}return table.concat({tostring(update.revision or\"\"),tostring(update.status or\"\"),tostring(update.message or\"\"),tostring(transfer.received or\"\"),tostring(transfer.size or\"\"),tostring(cache.version or\"\"),tostring(cache.digest or\"\"),tostring(#(update.offers or{})),},\"\\31\")end local function craftingSnapshotKey(snapshot)if type(snapshot)~=\"table\"then return\"missing\"end local sources,config=snapshot.sources or{},snapshot.config or{}return table.concat({tostring(snapshot.revision or\"\"),tostring(config.ticker and(config.ticker.id or config.ticker.name or config.ticker.label)or\"\"),tostring(#(sources.tickers or{})),tostring(snapshot.updatedAt or\"\"),},\"\\31\")end function M.craftingSnapshotChanged(state,snapshot)return type(state)==\"table\"and craftingSnapshotKey(snapshot)~=state.craftingSnapshotKey end function M.modalBounds(width,height,desiredWidth,desiredHeight)width,height=math.max(1,tonumber(width)or 1),math.max(1,tonumber(height)or 1)local modalWidth=math.max(1,math.min(width-2,tonumber(desiredWidth)or width-2))local modalHeight=math.max(1,math.min(height-2,tonumber(desiredHeight)or height-2))return{width=modalWidth,height=modalHeight,x=math.max(1,math.floor((width-modalWidth)/2)+1),y=math.max(1,math.floor((height-modalHeight)/2)+1),}end function M.updateSnapshotChanged(state,update)return type(state)==\"table\"and updateSnapshotKey(update)~=state.updateSnapshotKey end function M.newState()return{selected=nil,desktopPage=1,menu=nil,path=\"/\",history={},trash=false,peripheralScroll=1,networkSnapshot={}}end function M.model(page,deps,state)state=state or M.newState()local items,lines,cards,details={},{},{},{}local auth,net,peripherals,telemetry,cloud=deps.auth,deps.net,deps.peripherals,deps.telemetry,deps.cloud if auth and auth.refreshSession then auth.refreshSession()end local account=auth.current and(auth.current.name..\" (\"..auth.current.role..\")\")or\"not logged in\"local liveNetwork=(store.readStable and store.readStable(\"/ceetos/data/network-status.lua\",nil))or store.read(\"/ceetos/data/network-status.lua\",nil)if type(liveNetwork)==\"table\"then state.networkSnapshot=liveNetwork else liveNetwork=state.networkSnapshot or{}end local operation=(store.readStable and store.readStable(\"/ceetos/data/operation-status.lua\",nil))or store.read(\"/ceetos/data/operation-status.lua\",nil)if type(operation)==\"table\"and(tonumber(operation.expires)or 0)>os.epoch(\"utc\")then state.operation=operation else state.operation=nil end local visiblePeers=liveNetwork.peers or net.peers()local craftState=(store.readStable and store.readStable(\"/ceetos/data/crafting-state.lua\",nil))or store.read(\"/ceetos/data/crafting-state.lua\",nil)if type(craftState)==\"table\"then state.craftingSnapshot=craftState else craftState=state.craftingSnapshot or{}end state.craftingSnapshotKey=craftingSnapshotKey(craftState)local craftSources=craftState.sources or{}local selectedTicker=craftState.config and craftState.config.ticker local craftingAvailable=selectedTicker~=nil or#(craftSources.tickers or{})>0 if not craftingAvailable and peripheral and peripheral.getNames then for _,name in ipairs(peripheral.getNames())do if tostring(peripheral.getType(name)or\"\"):lower():find(\"stockticker\",1,true)then craftingAvailable=true;break end end end if page==\"home\"then add(items,role(deps,\"operator\"),system(\"terminal\",\"Terminal\"))if role(deps,\"viewer\")then for _,program in ipairs(deps.files.programs())do items[#items+1]={id=\"file:\"..program.path,label=program.name,kind=\"program\",path=program.path,runnable=true,canRun=true,canModify=role(deps,\"operator\"),protected=program.protected,action=\"run:\"..program.path}end end if craftingAvailable then items[#items+1]=system(\"crafting\",\"Crafting\")end if role(deps,\"operator\")then items[#items+1]=system(\"page:files\",\"Files\")items[#items+1]=system(\"page:peers\",\"Peers\")items[#items+1]=system(\"page:peripherals\",\"Devices\")items[#items+1]=system(\"page:telemetry\",\"Telemetry\")items[#items+1]=system(\"page:jobs\",\"Jobs\")items[#items+1]=system(\"page:updates\",\"Updates\")end items[#items+1]=system(\"page:admin\",\"Admin\")return{page=page,title=\"CeetOS Desktop\",desktop=true,items=items,subtitle=account..\"  |  \"..#visiblePeers..\" network peers  |  \"..#peripherals.describe()..\" shared  |  telemetry \"..(telemetry.status()and\"on\"or\"off\"),}elseif page==\"files\"then local source=state.trash and deps.files.trashList()or deps.files.list(state.path)for _,entry in ipairs(source)do local kind=entry.directory and\"directory\"or(entry.runnable and\"program\"or\"file\")items[#items+1]={id=(entry.trashed and\"trash:\"..entry.id or\"file:\"..entry.path),label=entry.pending and(entry.name..\" (recovery pending)\")or entry.name,kind=kind,path=entry.path,original=entry.original,trashId=entry.id,parent=entry.parent,directory=entry.directory,runnable=entry.runnable,protected=entry.protected,trashed=entry.trashed,pending=entry.pending==true,canRun=role(deps,\"viewer\"),canModify=role(deps,\"operator\"),canRepair=role(deps,\"admin\"),action=entry.parent and\"up:\"..entry.path or(entry.directory and\"dir:\"..entry.path or(entry.runnable and role(deps,\"viewer\")and\"run:\"..entry.path or nil)),}end cards[#cards+1]=panel(state.trash and\"Trash\"or\"Location\",state.trash and\"Deleted items can be restored here.\"or short(state.path,48),state.trash and\"Permanent removal requires confirmation.\"or\"Open folders, run programs, or use M for file actions.\")return{page=page,title=\"CeetOS Explorer\",items=items,path=state.trash and\"Trash\"or state.path,trash=state.trash,cards=cards,itemTitle=state.trash and\"Trash items\"or\"Contents\",accessNote=accessNote(role(deps,\"operator\"),\"Read-only: sign in as an operator to modify files.\"),}elseif page==\"peers\"then local live=liveNetwork local diag=live.discovery or(net.discoveryStatus and net.discoveryStatus()or{})local session,transport=diag.session,diag.transport or live.transport or{}local peerList,directCount,routedCount,rekeyCount=visiblePeers,0,0,0 for _,peer in ipairs(peerList)do if peer.routed then routedCount=routedCount+1 else directCount=directCount+1 end if peer.rekeyRequired then rekeyCount=rekeyCount+1 end end local discoveryActive=diag.active==true or session~=nil local discoveryState=discoveryActive and tostring((session and session.state)or diag.state or\"inactive\")or\"inactive\"local discoveryText=\"Discovery: \"..discoveryState if session and tonumber(session.expires)then local remaining=math.max(0,math.ceil((tonumber(session.expires)-os.epoch(\"utc\"))/1000))discoveryText=discoveryText..\" (\"..tostring(remaining)..\"s remaining)\"end if session and session.retries then discoveryText=discoveryText..\" (retries \"..tostring(session.retries)..\")\"end if session and session.candidate then discoveryText=discoveryText..\" - candidate \"..tostring(session.candidate.label or session.candidate.id)end if state.pairingPending then discoveryText=\"Discovery: confirming \"..tostring(state.pairingPending.label or state.pairingPending.peer or\"peer\")end local modemText=transport.wireless and(\"Wireless links: \"..tostring(transport.wirelessLinkCount or 1))or\"No wireless modem attached\"local mesh=transport.mesh or{}lines={\"Network peers: \"..(directCount+routedCount)..\" (\"..directCount..\" direct, \"..routedCount..\" routed)\",discoveryText,modemText,\"Protocol: v\"..tostring(live.protocol or transport.protocol or 2)..\" | Rekey required: \"..tostring(rekeyCount),\"Links: \"..tostring(transport.healthyLinks or 0)..\" healthy / \"..tostring(transport.degradedLinks or 0)..\" degraded / \"..tostring(transport.offlineLinks or 0)..\" offline\",\"Mesh: \"..(mesh.enrolled and(\"enrolled e\"..tostring(mesh.epoch))or\"enrollment pending\"),\"Last receive distance: \"..tostring(transport.lastDistance or\"-\")}cards[#cards+1]=panel(\"Network\",tostring(directCount+routedCount)..\" members  |  \"..tostring(directCount)..\" direct  |  \"..tostring(routedCount)..\" routed\",tostring(transport.healthyLinks or 0)..\" healthy links  |  \"..tostring(rekeyCount)..\" need rekeying\")cards[#cards+1]=panel(\"Discovery\",discoveryText,modemText)for _,peer in ipairs(peerList)do local kind=peer.routed and\"Routed\"or\"Direct\"local route=peer.routed and(\" via \"..tostring(peer.next or\"?\")..\" (\"..tostring(peer.hops or\"?\")..\" hop)\")or(peer.auto and\" mesh edge\"or\"\")lines[#lines+1]=kind..\": \"..tostring(peer.label or(\"Computer \"..tostring(peer.id)))..\" (\"..tostring(peer.id)..\")\"..route..\" [\"..tostring(peer.health or\"unknown\")..\"]\"details[#details+1]=kind..\"  \"..tostring(peer.label or(\"Computer \"..tostring(peer.id)))..route..\"  \"..tostring(peer.health or\"unknown\")end if not discoveryActive and diag.lastState and diag.lastState~=\"inactive\"then lines[#lines+1]=\"Previous discovery: \"..tostring(diag.lastState)..(diag.lastMessage and(\" - \"..tostring(diag.lastMessage))or\"\")elseif diag.message then lines[#lines+1]=\"Network: \"..tostring(diag.message)end local canBegin=not discoveryActive add(items,role(deps,\"admin\")and canBegin,grouped(system(\"start_discovery\",\"Start discovery\"),\"Discovery actions\"))add(items,role(deps,\"admin\")and canBegin,grouped(system(\"join_discovery\",\"Join discovery\"),\"Discovery actions\"))add(items,role(deps,\"admin\")and discoveryState==\"candidate\",grouped(system(\"confirm_discovery\",\"Confirm candidate\"),\"Discovery actions\"))add(items,role(deps,\"admin\")and not canBegin,grouped(system(\"cancel_discovery\",\"Cancel discovery\"),\"Discovery actions\"))add(items,role(deps,\"operator\"),grouped(system(\"remote_refresh\",\"Refresh remote peripherals\"),\"Network tools\"))elseif page==\"peripherals\"then local shared={}for _,item in ipairs(peripherals.describe())do shared[item.name]=item.mode end local detected=peripherals.listLocal()lines={\"Detected devices: \"..#detected,\"Shared devices: \"..#peripherals.describe()}cards[#cards+1]=panel(\"Devices\",tostring(#detected)..\" detected  |  \"..tostring(#peripherals.describe())..\" shared\",\"Select View peripherals to open the sharing matrix.\")cards[#cards+1]=panel(\"Remote access\",\"Peer cache and controlled calls stay separate from sharing.\",role(deps,\"operator\")and\"Use the action list below to inspect or call a device.\"or\"Read-only: operator access is required for device controls.\")if not state.peripheralView then add(items,role(deps,\"operator\"),system(\"peripheral_view\",\"View peripherals\"))end for _,item in ipairs(detected)do local mode=shared[item.name]items[#items+1]={id=\"peripheral_check:\"..item.name..\":peer\",name=item.name,label=item.name,type=item.type,kind=\"peripheral\",sharedMode=mode,mode=\"peer\",checked=mode==\"peer\",toggle=true,action=\"peripheral_set:\"..item.name..\":peer\"}items[#items+1]={id=\"peripheral_check:\"..item.name..\":master\",name=item.name,label=item.name,type=item.type,kind=\"peripheral\",sharedMode=mode,mode=\"master\",checked=mode==\"master\",toggle=true,action=\"peripheral_set:\"..item.name..\":master\"}end add(items,role(deps,\"operator\"),system(\"share_all:peer\",\"Share all - peers\"))add(items,role(deps,\"operator\"),system(\"share_all:master\",\"Share all - master only\"))add(items,role(deps,\"operator\"),system(\"unshare_all\",\"Unshare all\"))add(items,role(deps,\"operator\"),system(\"remote_cache\",\"View remote cache\"))add(items,role(deps,\"operator\"),system(\"remote_call\",\"Call remote peripheral\"))local shown={}for _,entry in ipairs(items)do local checkbox=entry.id:sub(1,17)==\"peripheral_check:\"local bulk=entry.action==\"share_all:peer\"or entry.action==\"share_all:master\"or entry.action==\"unshare_all\"if state.peripheralView or(not checkbox and not bulk)then shown[#shown+1]=entry end end items=shown elseif page==\"telemetry\"then local c=telemetry.configuration()local cloudStatus=cloud and cloud.status and cloud.status()or{}lines={\"Telemetry: \"..(c.enabled and\"enabled\"or\"disabled\"),\"Cloud node: \"..(cloudStatus.enrolled and(\"enrolled as \"..tostring(cloudStatus.node))or\"not enrolled\"),\"Cloud auth: \"..(cloudStatus.authOnline and\"online\"or\"offline\")..\" | recipes: \"..(cloudStatus.recipeOnline and\"online\"or\"offline\"),cloudStatus.enrolled and(\"Last report: \"..tostring(cloudStatus.telemetryLastSuccess or\"pending\")..\" | next: \"..tostring(cloudStatus.telemetryNext or\"pending\"))or\"Enroll this computer before it can appear on dash.ceet.uk.\",cloudStatus.telemetryLastError and(\"Telemetry error: \"..short(cloudStatus.telemetryLastError,58))or\"Cloud telemetry uploads daily and does not block this UI.\",}cards[#cards+1]=panel(\"Telemetry\",c.enabled and\"Collection is enabled\"or\"Collection is disabled\",cloudStatus.enrolled and(\"Cloud node: \"..tostring(cloudStatus.node))or\"Cloud node: not enrolled\")cards[#cards+1]=panel(\"Cloud health\",\"Auth: \"..(cloudStatus.authOnline and\"online\"or\"offline\")..\"  |  Recipes: \"..(cloudStatus.recipeOnline and\"online\"or\"offline\"),cloudStatus.telemetryLastError and(\"Last error: \"..short(cloudStatus.telemetryLastError,30))or\"No current cloud error\")add(items,role(deps,\"admin\"),grouped(system(\"telemetry_toggle\",c.enabled and\"Disable telemetry\"or\"Enable telemetry\"),\"Telemetry controls\"))add(items,role(deps,\"admin\")and not cloudStatus.enrolled,grouped(system(\"cloud_enroll\",\"Enroll this computer with CeetOS Cloud\"),\"Cloud enrollment\"))add(items,role(deps,\"operator\")and cloudStatus.enrolled,grouped(system(\"cloud_retry\",\"Retry cloud sync now\"),\"Cloud enrollment\"))add(items,role(deps,\"admin\")and cloudStatus.enrolled,grouped(system(\"cloud_clear\",\"Remove cloud enrollment\"),\"Cloud enrollment\"))elseif page==\"updates\"then local update=(store.readStable and store.readStable(\"/ceetos/data/update-status.lua\",nil))or store.read(\"/ceetos/data/update-status.lua\",{})state.updateSnapshotKey=updateSnapshotKey(update)local cache,transfer=update.cache,update.transfer lines={\"Installed: \"..tostring(update.version or\"unknown\")..\"  |  \"..tostring(update.status or\"idle\"),update.message and short(update.message,72)or\"Only locally confirmed signed releases can be applied.\",cache and(\"Verified cache: \"..tostring(cache.version)..\"  \"..tostring(cache.fingerprint or\"-\")..\"  key \"..tostring(cache.keyId or\"-\"))or\"Verified cache: none\",transfer and(\"Downloading: \"..tostring(transfer.received or 0)..\"/\"..tostring(transfer.size or\"?\")..\" bytes from \"..tostring(transfer.label or transfer.source))or\"No active download\",}cards[#cards+1]=panel(\"Release state\",\"Installed: \"..tostring(update.version or\"unknown\"),tostring(update.status or\"idle\"))cards[#cards+1]=panel(\"Verified cache\",cache and(\"Version \"..tostring(cache.version)..\"  |  key \"..tostring(cache.keyId or\"-\"))or\"No verified release cached\",transfer and(\"Downloading \"..tostring(transfer.received or 0)..\"/\"..tostring(transfer.size or\"?\")..\" bytes\")or\"Check sources or select a signed offer below.\")add(items,role(deps,\"operator\"),system(\"update_check\",\"Check for signed releases\"))add(items,role(deps,\"operator\"),system(\"update_install_latest\",\"Install latest signed release\"))for _,offer in ipairs(update.offers or{})do local route=offer.transport==\"cloud\"and\"cloud\"or(offer.transport==\"routed\"and\"routed\"or\"direct\")items[#items+1]={id=\"update-source:\"..tostring(offer.key or offer.id),label=\"Download \"..tostring(offer.version)..\" from \"..short(offer.label or offer.id,18)..\" (\"..route..\")\",kind=\"update\",action=\"update_download:\"..tostring(offer.key or offer.id),offer=offer}end add(items,role(deps,\"operator\")and transfer~=nil,system(\"update_cancel\",\"Cancel download\"))add(items,role(deps,\"operator\")and cache~=nil,system(\"update_apply\",\"Apply verified cached release\"))add(items,role(deps,\"operator\")and cache~=nil and transfer==nil,system(\"update_clear\",\"Clear release cache\"))elseif page==\"admin\"then lines={\"Session: \"..account}cards[#cards+1]=panel(\"Current session\",auth.current and(\"Signed in as \"..tostring(auth.current.name))or\"Not signed in\",auth.current and(\"Role: \"..tostring(auth.current.role))or\"Sign in to access your permissions\")cards[#cards+1]=panel(\"Account authority\",\"Accounts: \"..tostring(#auth.list()),role(deps,\"operator\")and\"Account tools are available below.\"or\"Operator access is required for account tools.\")add(items,auth.current==nil,grouped(system(\"login\",\"Sign in\"),\"Session\"))add(items,auth.current~=nil,grouped(system(\"logout\",\"Sign out\"),\"Session\"))if role(deps,\"operator\")then for _,user in ipairs(auth.list())do items[#items+1]={id=\"account:\"..user.name,label=user.name..\"  (\"..user.role..\")\"..((auth.current and auth.current.name==user.name)and\"  current\"or\"\"),kind=\"account\",group=\"Accounts\",name=user.name,userRole=user.role,canGrantAdmin=role(deps,\"admin\"),active=auth.current and auth.current.name==user.name}end add(items,true,grouped(system(\"user_add\",\"Create user\"),\"Account tools\"))add(items,true,grouped(system(\"recovery_enable\",\"Enable recovery window\"),\"Recovery\"))add(items,true,grouped(system(\"recovery_reset\",\"Recovery password reset\"),\"Recovery\"))add(items,true,grouped(system(\"page:audit\",\"View audit log\"),\"Review\"))add(items,true,grouped(system(\"page:jobs\",\"Open Jobs\"),\"Review\"))end elseif page==\"audit\"then lines={\"Latest audit events:\"}cards[#cards+1]=panel(\"Audit log\",\"Most recent local account and service events\",\"Use Backspace to return to Administration.\")for _,event in ipairs(deps.audit.recent(10))do lines[#lines+1]=event.actor..\" - \"..event.action;details[#details+1]=event.actor..\"  \"..event.action end elseif page==\"jobs\"then store.invalidate(\"/ceetos/data/jobs.lua\")local jobState=store.readStable(\"/ceetos/data/jobs.lua\",{})local scheduler=type(jobState)==\"table\"and jobState.schema==2 and type(jobState.data)==\"table\"and jobState.data or jobState local controller=scheduler.controller or scheduler.controllerId or scheduler.masterId local workers=scheduler.workers or{}local queue=scheduler.queue or{}local schedules=scheduler.schedules or{}local history=scheduler.history or scheduler.results or{}lines={\"Controller: \"..tostring(controller or\"electing / unavailable\"),\"Workers: \"..count(workers)..\"  |  Queue: \"..count(queue),\"Schedules: \"..count(schedules)..\"  |  Recent results: \"..count(history),role(deps,\"admin\")and\"Administrator view: scheduler controls are available through nx jobs.\"or\"Operator view: scheduler state is read-only.\",}cards[#cards+1]=panel(\"Scheduler\",\"Controller: \"..tostring(controller or\"electing / unavailable\"),tostring(count(workers))..\" workers  |  \"..tostring(count(queue))..\" queued\")cards[#cards+1]=panel(\"Schedules\",tostring(count(schedules))..\" scheduled  |  \"..tostring(count(history))..\" recent results\",role(deps,\"admin\")and\"Use nx jobs for scheduler controls.\"or\"This role can view scheduler state.\")for _,worker in ipairs(entries(workers))do local name=worker.label or worker.name or worker.id or worker._ceetosKey local active=worker.inFlight or worker.active or 0 local capacity=worker.capacity or 1 local health=worker.available==false and\"offline\"or(worker.health or\"ready\")items[#items+1]={id=\"job-worker:\"..tostring(worker.id or worker._ceetosKey),label=short(name,18)..\"  \"..tostring(active)..\"/\"..tostring(capacity)..\"  \"..short(health,10),kind=\"job\",group=\"Workers\"}end for _,schedule in ipairs(entries(schedules))do local name=schedule.name or schedule.template or schedule.id or schedule._ceetosKey local interval=schedule.interval or schedule.every or\"once\"local status=schedule.enabled==false and\"paused\"or(schedule.status or\"scheduled\")items[#items+1]={id=\"job-schedule:\"..tostring(schedule.id or schedule._ceetosKey),label=short(name,17)..\"  \"..short(interval,10)..\"  \"..short(status,10),kind=\"job\",group=\"Schedules\"}end for _,result in ipairs(entries(history))do local name=result.template or result.name or result.jobId or result.id or result._ceetosKey local status=result.status or(result.ok==false and\"failed\"or\"complete\")local worker=result.worker or result.workerId or\"-\"items[#items+1]={id=\"job-result:\"..tostring(result.id or result._ceetosKey),label=short(name,15)..\"  \"..short(status,10)..\"  \"..short(worker,10),kind=\"job\",group=\"Recent results\"}end end return{page=page,title=\"CeetOS | \"..(M.pages[page]or page),items=items,lines=lines,cards=cards,details=details,updates=page==\"updates\",remoteResult=page==\"peripherals\"and state.remoteResult or nil,itemTitle=page==\"peers\"and\"Network members\"or(page==\"telemetry\"and\"Available actions\"or(page==\"audit\"and\"Recent events\"or(page==\"peripherals\"and\"Device actions\"or nil))),accessNote=page==\"peers\"and accessNote(role(deps,\"operator\"),\"Read-only: operator access is required to pair or refresh peers.\")or page==\"admin\"and accessNote(role(deps,\"operator\"),\"Sign in as an operator to manage users.\")or page==\"peripherals\"and accessNote(role(deps,\"operator\"),\"Read-only: operator access is required to share or call devices.\"),}end local function indexOf(model,id)for index,item in ipairs(model.items)do if item.id==id then return index end end end function M.selected(model,state)if#model.items==0 then return nil end local index=indexOf(model,state.selected)if not index then index=1;state.selected=model.items[1].id end return model.items[index],index end function M.select(model,state,item)if item then state.selected=item.id end return item end function M.move(model,state,direction)local _,index=M.selected(model,state)if not index then return end local delta=direction==\"left\"and-1 or direction==\"right\"and 1 or direction==\"up\"and-(model.columns or 1)or(model.columns or 1)local nextIndex=math.max(1,math.min(#model.items,index+delta))state.selected=model.items[nextIndex].id end function M.context(item)if not item or item.parent then return{}end local copy=item.label and{system(\"copy_label:\"..(item.id or\"\"),\"Copy text\")}or{}if item.kind==\"account\"then local actions={system(\"user_role:\"..item.name..\":viewer\",\"Set role: viewer\"),system(\"user_role:\"..item.name..\":operator\",\"Set role: operator\")}if item.canGrantAdmin then actions[#actions+1]=system(\"user_role:\"..item.name..\":admin\",\"Set role: admin\")end actions[#actions+1]=system(\"user_reset:\"..item.name,\"Reset password\")actions[#actions+1]=system(\"user_delete:\"..item.name,\"Delete user\")for _,action in ipairs(copy)do actions[#actions+1]=action end return actions end if item.kind==\"peripheral\"then local actions={}if item.sharedMode then actions[#actions+1]=system(\"peripheral_set:\"..item.name..\":peer\",\"Share with peers\")actions[#actions+1]=system(\"peripheral_set:\"..item.name..\":master\",\"Share with master only\")actions[#actions+1]=system(\"peripheral_unshare:\"..item.name,\"Stop sharing\")else actions[#actions+1]=system(\"peripheral_set:\"..item.name..\":peer\",\"Share with peers\")actions[#actions+1]=system(\"peripheral_set:\"..item.name..\":master\",\"Share with master only\")end return actions end if item.trashed then if item.pending then if item.canRepair then copy[#copy+1]=system(\"repair_trash\",\"Repair pending Trash transaction\")end elseif item.canModify then copy[#copy+1]=system(\"restore:\"..item.trashId,\"Restore\")copy[#copy+1]=system(\"delete_trash:\"..item.trashId,\"Delete permanently\")end return copy end if not item.path then if item.action then copy[#copy+1]=system(item.action,\"Open\")end;return copy end local actions={}if item.action and(not item.runnable or item.canRun~=false)then actions[#actions+1]=system(item.action,item.directory and\"Open\"or\"Run\")end if not item.protected and item.canModify then if not item.directory then actions[#actions+1]=system(\"edit:\"..item.path,\"Edit\")end actions[#actions+1]=system(\"rename:\"..item.path,\"Rename\")actions[#actions+1]=system(\"trash:\"..item.path,\"Move to Trash\")end for _,action in ipairs(copy)do actions[#actions+1]=action end return actions end local function drawHeader(model,state,banner,width,height)local bannerText=banner and terminalUi.clean(banner.text)or\"\"local operation=state.operation local progress=operation and tonumber(operation.progress)or 0 local operationText=operation and tostring(operation.name or\"Loading\")or\"\"local key=table.concat({tostring(width),tostring(height),model.title or\"\",model.subtitle or model.path or\"\",bannerText,banner and tostring(banner.error)or\"\",operationText,tostring(progress)},\"\\31\")if state._headerKey==key then return 4 end terminalUi.header(model.title,progress)if height>=2 then terminalUi.clearLine(2,colors.black)terminalUi.write(2,2,model.subtitle or model.path or\"\",colors.lightGray,colors.black,width-2)end if height>=3 then terminalUi.clearLine(3,colors.black)if bannerText~=\"\"then terminalUi.write(2,3,bannerText,banner.error and colors.red or colors.lightGray,colors.black,width-2)elseif operationText~=\"\"then terminalUi.write(2,3,operationText..\" \"..tostring(math.floor(progress*100))..\"%\",colors.lightGray,colors.black,width-2)end end state._headerKey=key return 4 end local function drawRemoteResult(result,top,width,height,targets)if not result or top>=height then return top end local pending=result.pending==true or result.status==\"pending\"local failed=result.error==true or result.ok==false or result.status==\"error\"local label=pending and\"Remote request pending\"or(failed and\"Remote request failed\"or\"Remote result\")if result.peer then label=label..\" | \"..tostring(result.peer)end if result.device then label=label..\" | \"..tostring(result.device)end if result.method then label=label..\".\"..tostring(result.method)end terminalUi.clearLine(top,failed and colors.black or colors.gray)terminalUi.write(2,top,label,failed and colors.red or colors.white,failed and colors.black or colors.gray,width-4)if not pending then local clearLabel,copyLabel=\"[Clear]\",\"[Copy]\"local clearX,copyX=width-#clearLabel-1,width-#clearLabel-#copyLabel-3 terminalUi.write(copyX,top,copyLabel,colors.lightBlue,colors.gray)terminalUi.write(clearX,top,clearLabel,colors.lightBlue,colors.gray)targets[#targets+1]={kind=\"item\",item=system(\"remote_result_copy\",\"Copy remote result\"),x1=copyX,x2=copyX+#copyLabel-1,y1=top,y2=top}targets[#targets+1]={kind=\"item\",item=system(\"remote_result_clear\",\"Clear remote result\"),x1=clearX,x2=clearX+#clearLabel-1,y1=top,y2=top}end if top+1<height then terminalUi.write(2,top+1,result.text or result.message or result.value or\"\",failed and colors.red or colors.lightGray,colors.black,width-3)end return top+2 end local function drawMenu(menu,width,height)if not menu or#menu.items==0 then return{}end local longest=0 for _,item in ipairs(menu.items)do longest=math.max(longest,#terminalUi.clean(item.label))end local boxWidth,x=math.max(8,math.min(width-2,longest+4)),math.max(1,width-math.max(8,math.min(width-2,longest+4)))local y=math.max(2,height-#menu.items-1)local targets={}for index,item in ipairs(menu.items)do local selected=index==menu.selected terminalUi.fill(x,y+index-1,boxWidth,1,selected and colors.gray or colors.black)terminalUi.write(x+1,y+index-1,item.label,selected and colors.white or colors.lightGray,selected and colors.gray or colors.black,boxWidth-2)targets[#targets+1]={kind=\"menu\",item=item,x1=x,x2=x+boxWidth-1,y1=y+index-1,y2=y+index-1}end return targets end local function desktopSignature(model,first,last,columns,cellWidth,top)local fields={tostring(first),tostring(last),tostring(columns),tostring(cellWidth),tostring(top)}for index=first,last do local item=model.items[index]fields[#fields+1]=table.concat({tostring(item.id),tostring(item.label),tostring(item.kind)},\"\\30\")end return table.concat(fields,\"\\31\")end local function drawDesktop(model,state,banner,width,height)if width<24 or height<9 then return terminalUi.minimum(model.title,\"Desktop needs 24x9\",\"Resize | Backspace\")end local top=drawHeader(model,state,banner,width,height)+1 local columns=math.max(2,math.floor((width-2)/14))local cellWidth,cellHeight=math.floor((width-2)/columns),4 local rows,capacity=math.max(1,math.floor((height-top-1)/cellHeight)),9 capacity=math.min(capacity,columns*rows)model.columns=columns local item,selectedIndex=M.selected(model,state)local totalPages=math.max(1,math.ceil(#model.items/capacity))state.desktopPage=math.max(1,math.min(math.floor((selectedIndex-1)/capacity)+1,totalPages))local first,last=(state.desktopPage-1)*capacity+1,math.min(#model.items,state.desktopPage*capacity)local signature=desktopSignature(model,first,last,columns,cellWidth,top)local prior=state._desktopRender or{}local rebuild=prior.signature~=signature or prior.width~=width or prior.height~=height if rebuild then terminalUi.fill(1,4,width,height-3,colors.black)end local targets={}local function drawTile(index)local slot,entry=index-first,model.items[index]if not entry then return end local x,y=2+(slot%columns)*cellWidth,top+math.floor(slot/columns)*cellHeight local selected=entry.id==state.selected terminalUi.fill(x,y,cellWidth,cellHeight,selected and colors.gray or colors.black)terminalUi.write(x+1,y+1,(selected and\">\"or\" \")..tostring(entry.label or\"\"),selected and colors.white or colour(entry),selected and colors.gray or colors.black,cellWidth-2)terminalUi.write(x+1,y+2,selected and\" Press Enter\"or\"\",selected and colors.lightGray or colors.black,selected and colors.gray or colors.black,cellWidth-2)end for index=first,last do local slot,entry=index-first,model.items[index]local x,y=2+(slot%columns)*cellWidth,top+math.floor(slot/columns)*cellHeight if rebuild or entry.id==state.selected or entry.id==prior.selected then drawTile(index)end targets[#targets+1]={kind=\"item\",item=entry,x1=x,x2=x+cellWidth-1,y1=y,y2=y+cellHeight-1}end local footer=\"WASD / Arrows select | Enter open | M menu | [\"..state.desktopPage..\"/\"..totalPages..\"] | Q desktop\"if rebuild or prior.footer~=footer then terminalUi.clearLine(height,colors.black)terminalUi.write(2,height,footer,colors.gray,colors.black,width-2)end state._desktopRender={signature=signature,width=width,height=height,selected=state.selected,footer=footer}return targets end local drawCards local function drawUpdates(model,state,banner,width,height)if width<24 or height<12 then return terminalUi.minimum(model.title,\"Updates needs 24x12\",\"Resize | Backspace\")end local top,footer=drawHeader(model,state,banner,width,height)+1,height-1 terminalUi.fill(1,4,width,height-3,colors.black)top=drawCards(model.cards or{},top,width,footer-4)local summary=(model.lines or{})[3]or(model.lines or{})[2]if summary and top<footer-4 then terminalUi.write(2,top,summary,colors.lightGray,colors.black,width-2)top=top+2 elseif#model.cards==0 then for _,line in ipairs(model.lines or{})do if top>=footer-4 then break end terminalUi.write(2,top,line,colors.lightGray,colors.black,width-2)top=top+1 end top=top+1 end local columns=math.max(2,math.floor((width-2)/16))local cellWidth,cellHeight=math.floor((width-2)/columns),3 local rows=math.max(1,math.floor((footer-top)/cellHeight))local capacity=math.max(1,columns*rows)model.columns=columns local _,selectedIndex=M.selected(model,state)local totalPages=math.max(1,math.ceil(#model.items/capacity))state.updatePage=math.max(1,math.min(math.floor(((selectedIndex or 1)-1)/capacity)+1,totalPages))local first,last=(state.updatePage-1)*capacity+1,math.min(#model.items,state.updatePage*capacity)local targets={}for index=first,last do local slot,entry=index-first,model.items[index]local x,y=2+(slot%columns)*cellWidth,top+math.floor(slot/columns)*cellHeight local selected=entry.id==state.selected terminalUi.fill(x,y,cellWidth,cellHeight,selected and colors.gray or colors.black)terminalUi.write(x+1,y+1,(selected and\">\"or\" \")..tostring(entry.label or\"\"),selected and colors.white or colors.lightBlue,selected and colors.gray or colors.black,cellWidth-2)terminalUi.write(x+1,y+2,selected and\" Press Enter\"or\"\",selected and colors.lightGray or colors.black,selected and colors.gray or colors.black,cellWidth-2)targets[#targets+1]={kind=\"item\",item=entry,x1=x,x2=x+cellWidth-1,y1=y,y2=y+cellHeight-1}end terminalUi.clearLine(height,colors.black)terminalUi.write(2,height,\"WASD / Arrows select | Enter run | Backspace desktop | [\"..state.updatePage..\"/\"..totalPages..\"]\",colors.gray,colors.black,width-2)return targets end local function cardsSignature(cards)local values={}for _,card in ipairs(cards or{})do values[#values+1]=terminalUi.clean(card.title)for _,line in ipairs(card.lines or{})do values[#values+1]=terminalUi.clean(line)end values[#values+1]=\"\\30\"end return table.concat(values,\"\\31\")end drawCards=function(cards,top,width,bottom)local count=math.min(2,#(cards or{}))if count==0 or top+3>=bottom then return top end local columns=width>=46 and count>1 and 2 or 1 local cardWidth=math.floor((width-columns-1)/columns)local rows=math.ceil(count/columns)for index=1,count do local card=cards[index]local column,row=(index-1)%columns,math.floor((index-1)/columns)local x,y=2+column*(cardWidth+1),top+row*4 terminalUi.box(x,y,x+cardWidth-1,y+3,card.title,colors.gray,colors.lightBlue)for lineIndex=1,2 do local line=card.lines and card.lines[lineIndex]if line then terminalUi.write(x+2,y+lineIndex,line,colors.lightGray,colors.black,cardWidth-4)end end end return top+rows*4 end local function cardsBottom(cards,top,width,bottom)local count=math.min(2,#(cards or{}))if count==0 or top+3>=bottom then return top end local columns=width>=46 and count>1 and 2 or 1 return top+math.ceil(count/columns)*4 end local function visualRows(model)local rows,byIndex,lastGroup={},{},nil local detailLines=type(model.details)==\"table\"and#model.details>0 and model.details or(model.lines or{})for _,line in ipairs(detailLines)do rows[#rows+1]={kind=\"detail\",label=line}end if#(model.items or{})>0 and model.itemTitle and lastGroup==nil then rows[#rows+1]={kind=\"heading\",label=model.itemTitle}end for index,item in ipairs(model.items or{})do if item.group and item.group~=lastGroup then rows[#rows+1]={kind=\"heading\",label=item.group}lastGroup=item.group end local row={kind=\"item\",item=item,index=index}rows[#rows+1]=row byIndex[index]=#rows end if#rows==0 and model.accessNote then rows[#rows+1]={kind=\"detail\",label=model.accessNote,empty=true}end return rows,byIndex end local function drawListRow(row,y,selected,width)if row.kind==\"heading\"then terminalUi.clearLine(y,colors.black)terminalUi.write(2,y,terminalUi.clean(row.label),colors.lightBlue,colors.black,width-3)return end if row.kind==\"detail\"then terminalUi.clearLine(y,colors.black)terminalUi.write(3,y,terminalUi.clean(row.label),row.empty and colors.gray or colors.lightGray,colors.black,width-4)return end local item=row.item terminalUi.clearLine(y,selected and colors.gray or colors.black)terminalUi.write(2,y,selected and\">\"or\" \",selected and colors.white or colour(item),selected and colors.gray or colors.black,1)terminalUi.write(4,y,item.label,selected and colors.white or colour(item),selected and colors.gray or colors.black,width-5)end local function listSignature(model,rows,first,top,footer,width,height)local remote=model.remoteResult local values={tostring(model.page),tostring(first),tostring(top),tostring(footer),tostring(width),tostring(height),cardsSignature(model.cards),terminalUi.clean(model.accessNote or\"\"),terminalUi.clean(model.itemTitle or\"\"),remote and tostring(remote.status or\"\")or\"\",remote and terminalUi.clean(remote.text or\"\")or\"\",}for _,row in ipairs(rows)do values[#values+1]=row.kind values[#values+1]=terminalUi.clean(row.label or(row.item and row.item.label)or\"\")values[#values+1]=row.item and tostring(row.item.id)or\"\"end return table.concat(values,\"\\31\")end local function footerFor(model)if model.page==\"files\"then return model.trash and\"WASD / Arrows select | Enter action | M menu | E empty Trash | Backspace Explorer\"or\"WASD / Arrows select | Enter open | M menu | N folder | T Trash | Backspace up\"elseif model.page==\"peers\"then return\"WASD / Arrows select | Enter action | M menu | R refresh | Backspace desktop\"elseif model.page==\"admin\"then return\"WASD / Arrows select | Enter action | M account actions | Backspace desktop\"elseif model.page==\"audit\"then return\"Backspace Administration | R refresh\"end return\"WASD / Arrows select | Enter action | M menu | Backspace desktop\"end local function drawList(model,state,banner,width,height)if width<24 or height<8 then return terminalUi.minimum(model.title,\"Screen needs 24x8\",\"Resize | Backspace\")end local targets={}local top,footer=drawHeader(model,state,banner,width,height)+1,height-1 local remote=model.page==\"peripherals\"and model.remoteResult or nil if remote then top=top+3 end local renderCards=model.cards or{}local minimumRows=1+((model.accessNote and model.accessNote~=\"\")and 1 or 0)if cardsBottom(renderCards,top,width,footer)>footer-minimumRows then renderCards={}end top=cardsBottom(renderCards,top,width,footer)if model.accessNote and top<footer then top=top+1 end local rows,byIndex=visualRows(model)local _,selectedIndex=M.selected(model,state)selectedIndex=selectedIndex or 1 local selectedRow=byIndex[selectedIndex]or 1 local visible=math.max(1,footer-top)local first=math.max(1,math.min(math.max(1,#rows-visible+1),selectedRow-math.floor(visible/2)))local signature=listSignature({page=model.page,cards=renderCards,accessNote=model.accessNote,itemTitle=model.itemTitle,remoteResult=remote,},rows,first,top,footer,width,height)local prior=state._listRender or{}local rebuild=prior.signature~=signature local rowLocations={}if rebuild then terminalUi.fill(1,4,width,height-3,colors.black)local drawTop=drawHeader(model,state,banner,width,height)+1 if remote then drawTop=drawRemoteResult(remote,drawTop,width,footer,targets)+1 end drawTop=drawCards(renderCards,drawTop,width,footer)if model.accessNote and drawTop<footer then terminalUi.write(2,drawTop,model.accessNote,colors.gray,colors.black,width-3)drawTop=drawTop+1 end top=drawTop for row=0,visible-1 do local visual=rows[first+row]if not visual then break end local y=top+row drawListRow(visual,y,visual.item and visual.item.id==state.selected,width)if visual.item then rowLocations[visual.item.id]=y end end else rowLocations=prior.rowLocations or{}if prior.selected~=state.selected then for _,id in ipairs({prior.selected,state.selected})do local y=rowLocations[id]if y then local index=indexOf(model,id)if index then drawListRow({kind=\"item\",item=model.items[index],index=index},y,id==state.selected,width)end end end end for index=first,math.min(#rows,first+visible-1)do local visual=rows[index]if visual and visual.item then rowLocations[visual.item.id]=top+index-first end end end for row=0,visible-1 do local visual=rows[first+row]if visual and visual.item then local y=(rebuild and top+row)or(rowLocations[visual.item.id]or(top+row))targets[#targets+1]={kind=\"item\",item=visual.item,x1=1,x2=width,y1=y,y2=y}end end terminalUi.clearLine(height,colors.black)terminalUi.write(2,height,footerFor(model),colors.gray,colors.black,width-2)state._listRender={signature=signature,selected=state.selected,rowLocations=rowLocations}model.columns=1 return targets end local function drawPeripheralMatrix(model,state,banner,width,height)if width<36 or height<15 then return terminalUi.minimum(model.title,\"Devices matrix needs 36x15\",\"Resize | Backspace\")end local targets={}local top,footer=drawHeader(model,state,banner,width,height)+1,height-1 terminalUi.fill(1,4,width,height-3,colors.black)for _,line in ipairs(model.lines or{})do if top>=footer-1 then break end terminalUi.write(2,top,line,colors.lightGray,colors.black,width-2);top=top+1 end top=drawRemoteResult(model.remoteResult,top,width,footer,targets)+1 local devices,byKey={},{}for _,item in ipairs(model.items)do if item.id:sub(1,17)==\"peripheral_check:\"then if not byKey[item.name]then devices[#devices+1]={name=item.name,type=item.type or\"unknown\"};byKey[item.name]={}end byKey[item.name][item.mode]=item end end local controls={}for _,item in ipairs(model.items)do if item.kind==\"system\"and(item.action==\"share_all:peer\"or item.action==\"share_all:master\"or item.action==\"unshare_all\"or item.action==\"remote_cache\"or item.action==\"remote_call\")then controls[#controls+1]=item end end local nameX,masterX,peerX=2,width-11,width-17 local typeX=math.max(14,math.floor(width*0.45))local deviceRows=math.max(1,footer-top-#controls-2)local selectedDevice for index,device in ipairs(devices)do local row=byKey[device.name]if(row.peer and row.peer.id==state.selected)or(row.master and row.master.id==state.selected)then selectedDevice=index;break end end if selectedDevice then state.peripheralScroll=math.max(1,math.min(math.max(1,#devices-deviceRows+1),selectedDevice-math.floor(deviceRows/2)))end state.peripheralScroll=math.max(1,math.min(math.max(1,#devices-deviceRows+1),state.peripheralScroll or 1))terminalUi.write(nameX,top,\"Name\",colors.lightGray,colors.black,typeX-nameX-1)terminalUi.write(typeX,top,\"Type\",colors.lightGray,colors.black,peerX-typeX-1)terminalUi.write(peerX,top,\"Peer\",colors.lightGray,colors.black,5)terminalUi.write(masterX,top,\"Master\",colors.lightGray,colors.black,7)for row=0,deviceRows-1 do local device=devices[state.peripheralScroll+row]if not device then break end local y=top+row+1 terminalUi.write(nameX,y,device.name,colors.white,colors.black,typeX-nameX-1)terminalUi.write(typeX,y,device.type,colors.lightGray,colors.black,peerX-typeX-1)for _,mode in ipairs({\"peer\",\"master\"})do local item=byKey[device.name][mode]local selected,x=item and state.selected==item.id,mode==\"peer\"and peerX or masterX terminalUi.write(x,y,item and(item.checked and\"[x]\"or\"[ ]\")or\"   \",item and item.checked and colors.lime or colors.lightGray,selected and colors.gray or colors.black,3)if item then targets[#targets+1]={kind=\"item\",item=item,x1=x,x2=x+2,y1=y,y2=y}end end end local y=top+deviceRows+1 for _,item in ipairs(controls)do if y>=footer then break end local selected=state.selected==item.id terminalUi.clearLine(y,selected and colors.gray or colors.black)terminalUi.write(2,y,item.label,selected and colors.white or colors.lightGray,selected and colors.gray or colors.black,width-2)targets[#targets+1]={kind=\"item\",item=item,x1=1,x2=width,y1=y,y2=y}y=y+1 end terminalUi.write(2,height,\"WASD / Arrows select | Enter toggle | Click checkbox | Backspace devices\",colors.gray,colors.black,width-2)return targets end function M.draw(model,state,banner)local width,height=term.getSize()local kind=model.desktop and\"desktop\"or(model.updates and\"updates\"or(model.page==\"peripherals\"and state.peripheralView and\"matrix\"or\"list\"))local menuKey=state.menu and table.concat({tostring(state.menu.selected),tostring(#(state.menu.items or{}))},\":\")or\"\"if state._viewKind~=kind or state._viewPage~=model.page then term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear()state._headerKey,state._desktopRender=nil,nil elseif state._menuKey~=menuKey then state._desktopRender=nil state._listRender=nil end state._viewKind,state._viewPage,state._menuKey=kind,model.page,menuKey local targets=model.desktop and drawDesktop(model,state,banner,width,height)or(model.updates and drawUpdates(model,state,banner,width,height)or(model.page==\"peripherals\"and state.peripheralView and drawPeripheralMatrix(model,state,banner,width,height)or drawList(model,state,banner,width,height)))local menuTargets=drawMenu(state.menu,width,height)for _,target in ipairs(menuTargets)do targets[#targets+1]=target end term.setBackgroundColor(colors.black);term.setTextColor(colors.white)return targets end function M.invalidate(state)if type(state)~=\"table\"then return end state._headerKey=nil state._desktopRender=nil state._listRender=nil state._viewKind=nil state._viewPage=nil state._menuKey=nil end function M.actionFor(model,number)if model.page==\"peers\"and number==2 then return{id=\"pair_offer\",label=\"Start discovery\"}end return model.items[number]end return M",
  ["ceetos/lib/terminal_ui.lua"] = "local M={}local function size()local width,height=term.getSize()return math.max(1,width or 1),math.max(1,height or 1)end function M.clean(value)return tostring(value or\"\"):gsub(\"%c\",\" \")end function M.clip(value,width,suffix)value,width=M.clean(value),math.max(0,math.floor(tonumber(width)or 0))if width==0 then return\"\"end if#value<=width then return value end suffix=suffix==nil and\"...\"or tostring(suffix)if#suffix>=width then return suffix:sub(1,width)end return value:sub(1,width-#suffix)..suffix end function M.write(x,y,value,foreground,background,width)local terminalWidth,terminalHeight=size()x,y=math.floor(tonumber(x)or 1),math.floor(tonumber(y)or 1)if y<1 or y>terminalHeight or x>terminalWidth then return false end if x<1 then value=M.clean(value):sub(2-x)x=1 end local available=math.max(0,terminalWidth-x+1)if width~=nil then available=math.min(available,math.max(0,math.floor(width)))end if available<=0 then return false end if foreground then term.setTextColor(foreground)end if background then term.setBackgroundColor(background)end term.setCursorPos(x,y)term.write(M.clip(value,available,\"\"))return true end function M.clearLine(y,background)local width,height=size()y=math.floor(tonumber(y)or 0)if y<1 or y>height then return false end term.setCursorPos(1,y)if background then term.setBackgroundColor(background)end term.clearLine()return width>0 end function M.header(title,progress)local width=size()local value=math.max(0,math.min(1,tonumber(progress)or 0))M.clearLine(1,colors.blue)local filled=math.floor(width*value)if filled>0 then M.fill(1,1,filled,1,colors.green)end local text=M.clip(title or\"CeetOS\",math.max(0,width-2),\"\")if text==\"\"then return end local foreground=(colors.toBlit and colors.toBlit(colors.white))or\"0\"local blue=(colors.toBlit and colors.toBlit(colors.blue))or\"b\"local green=(colors.toBlit and colors.toBlit(colors.green))or\"d\"local background={}for index=1,#text do background[index]=index+1<=filled and green or blue end term.setCursorPos(2,1)if term.blit then term.blit(text,foreground:rep(#text),table.concat(background))else M.write(2,1,text,colors.white,colors.blue,width-2)end end function M.fill(x,y,width,height,background)local terminalWidth,terminalHeight=size()x,y=math.max(1,math.floor(tonumber(x)or 1)),math.max(1,math.floor(tonumber(y)or 1))width,height=math.max(0,math.floor(tonumber(width)or 0)),math.max(0,math.floor(tonumber(height)or 0))width,height=math.min(width,terminalWidth-x+1),math.min(height,terminalHeight-y+1)if width<=0 or height<=0 then return false end term.setBackgroundColor(background or colors.black)for row=y,y+height-1 do term.setCursorPos(x,row);term.write((\" \"):rep(width))end return true end function M.box(x1,y1,x2,y2,title,border,titleColour)local width,height=size()x1,y1=math.max(1,math.floor(x1 or 1)),math.max(1,math.floor(y1 or 1))x2,y2=math.min(width,math.floor(x2 or width)),math.min(height,math.floor(y2 or height))if x2-x1<2 or y2-y1<2 then return false end border=border or colors.gray term.setBackgroundColor(colors.black);term.setTextColor(border)M.write(x1,y1,\"+\"..(\"-\"):rep(x2-x1-1)..\"+\",border,colors.black)for row=y1+1,y2-1 do M.write(x1,row,\"|\",border,colors.black);M.write(x2,row,\"|\",border,colors.black)end M.write(x1,y2,\"+\"..(\"-\"):rep(x2-x1-1)..\"+\",border,colors.black)if title and title~=\"\"then M.write(x1+2,y1,\" \"..M.clean(title)..\" \",titleColour or colors.lightBlue,colors.black,math.max(0,x2-x1-3))end return true end function M.minimum(title,message,footer)local width,height=size()term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear()M.clearLine(1,colors.blue);M.write(2,1,title or\"CeetOS\",colors.white,colors.blue,width-2)M.write(2,math.max(2,math.floor(height/2)),message or\"Terminal is too small\",colors.red,colors.black,width-2)M.write(2,height,footer or\"Resize terminal or press Backspace\",colors.gray,colors.black,width-2)return{}end function M.hit(targets,x,y)for index=#(targets or{}),1,-1 do local target=targets[index]if x>=target.x1 and x<=target.x2 and y>=target.y1 and y<=target.y2 then return target end end end return M",
  ["ceetos/lib/sounds.lua"] = "local M={}local POLICY={boot={instrument=\"chime\",volume=0.22,root=0.78,motif=\"047\",cooldown=1500},confirmation={instrument=\"bell\",volume=0.20,root=0.96,motif=\"07\",cooldown=500},reconnect={instrument=\"flute\",volume=0.17,root=0.82,motif=\"037\",cooldown=2500},warning={instrument=\"bass\",volume=0.22,root=0.72,motif=\"03\",cooldown=3000},error={instrument=\"didgeridoo\",volume=0.20,root=0.56,motif=\"036\",cooldown=3500},}local function generatedCue(event)local policy=POLICY[tostring(event or\"\")]if not policy then return nil end local notes={}for index=1,#policy.motif do local semitones=tonumber(policy.motif:sub(index,index))or 0 notes[#notes+1]={instrument=policy.instrument,volume=policy.volume*(1-((index-1)*0.12)),pitch=policy.root*(2^(semitones/12)),}end return notes end local function now()if not os or type(os.epoch)~=\"function\"then return 0 end local ok,value=pcall(os.epoch,\"utc\")return ok and math.max(0,math.floor(tonumber(value)or 0))or 0 end local function speakerNames()if not peripheral or type(peripheral.getNames)~=\"function\"or type(peripheral.getType)~=\"function\"then return{}end local ok,names=pcall(peripheral.getNames)if not ok or type(names)~=\"table\"then return{}end local found={}for _,name in ipairs(names)do local typed,kind=pcall(peripheral.getType,name)if typed and tostring(kind or\"\"):lower()==\"speaker\"then found[#found+1]=tostring(name)end end table.sort(found)return found end local SoundService={}SoundService.__index=SoundService function SoundService:refresh()self.speaker,self.name,self.checked=nil,nil,true if not peripheral or type(peripheral.wrap)~=\"function\"then return false,\"no peripheral API\"end for _,name in ipairs(speakerNames())do local ok,wrapped=pcall(peripheral.wrap,name)if ok and type(wrapped)==\"table\"and type(wrapped.playNote)==\"function\"then self.speaker,self.name=wrapped,name return true,name end end return false,\"no supported speaker\"end function SoundService:invalidate()self.checked,self.speaker,self.name=false,nil,nil end function SoundService:status()if not self.checked then self:refresh()end return{available=self.speaker~=nil,name=self.name}end function SoundService:play(event)local policy=POLICY[tostring(event or\"\")]if not policy then return false,\"unknown sound event\"end local timestamp=self.now()local previous=self.last[event]if previous and timestamp-previous<policy.cooldown then return false,\"cooldown\"end if not self.checked then self:refresh()end if not self.speaker then return false,\"no supported speaker\"end for _,note in ipairs(generatedCue(event)or{})do local ok,result=pcall(self.speaker.playNote,note.instrument,note.volume,note.pitch)if not ok or result==false then self:invalidate()return false,ok and\"speaker rejected note\"or tostring(result)end end self.last[event]=timestamp return true end function M.new(options)options=options or{}return setmetatable({now=type(options.now)==\"function\"and options.now or now,last={},speaker=nil,name=nil,checked=false,},SoundService)end M.policy=POLICY M.cue=generatedCue local default=M.new()function M.refresh()return default:refresh()end function M.invalidate()return default:invalidate()end function M.status()return default:status()end function M.play(event)return default:play(event)end return M",
  ["ceetos/lib/files.lua"] = "local store=require(\"ceetos.lib.store\")local auth=require(\"ceetos.lib.auth\")local disks=require(\"ceetos.lib.disks\")local M={}local TRASH_DIR=\"/ceetos/data/trash\"local TRASH_INDEX=\"/ceetos/data/trash-index.lua\"local TRASH_SCHEMA=2 local IMPORT_MAX_FILES,IMPORT_MAX_FILE_BYTES,IMPORT_MAX_TOTAL_BYTES,IMPORT_MAX_CHUNKS=32,1024*1024,8*1024*1024,4096 local protectedRoots={\"/ceetos\",\"/rom\"}local protectedFiles={[\"/startup.lua\"]=true}local function absolute(path)if type(path)~=\"string\"or path==\"\"or path==\"/\"then return\"/\"end local parts={}for part in path:gmatch(\"[^/]+\")do if part==\"..\"then if#parts>0 then table.remove(parts)end elseif part~=\".\"and part~=\"\"then parts[#parts+1]=part end end return#parts==0 and\"/\"or\"/\"..table.concat(parts,\"/\")end local function sort(items)table.sort(items,function(a,b)if a.directory~=b.directory then return a.directory end return a.name:lower()<b.name:lower()end)return items end local function availablePath(directory,name)name=fs.getName(name)local base,extension=name:match(\"^(.*)(%.[^.]*)$\")base,extension=base or name,extension or\"\"local path,index=fs.combine(directory,name),2 while fs.exists(path)do path=fs.combine(directory,base..\"-\"..index..extension)index=index+1 end return absolute(path)end local function rawRead(path,fallback)local function readOne(candidate)local handle=fs.exists(candidate)and fs.open(candidate,\"r\")or nil if not handle then return nil end local text=handle.readAll();handle.close()local value=textutils.unserialise(text)return type(value)==\"table\"and value or nil end return readOne(path)or readOne(path..\".bak\")or fallback end local function rawWrite(path,value)local temporary,backup=path..\".new\",path..\".bak\"if not fs.exists(path)and fs.exists(backup)then local prior=rawRead(backup,nil)if type(prior)~=\"table\"then return false,\"trash index backup is invalid\"end local restored=fs.open(path,\"w\")if not restored then return false,\"could not restore trash index backup\"end restored.write(textutils.serialise(prior));restored.close()end if fs.exists(temporary)then fs.delete(temporary)end local handle,openErr=fs.open(temporary,\"w\")if not handle then return false,openErr or\"could not open temporary index\"end local ok,writeErr=pcall(function()handle.write(textutils.serialise(value));handle.close()end)if not ok then if fs.exists(temporary)then fs.delete(temporary)end return false,writeErr end local verify=fs.open(temporary,\"r\")local valid=verify and textutils.unserialise(verify.readAll())or nil if verify then verify.close()end if type(valid)~=\"table\"then fs.delete(temporary)return false,\"temporary trash index is invalid\"end if fs.exists(backup)then fs.delete(backup)end if fs.exists(path)then local moved,moveErr=pcall(fs.move,path,backup)if not moved then fs.delete(temporary);return false,moveErr end end local activated,activateErr=pcall(fs.move,temporary,path)if not activated then if fs.exists(backup)and not fs.exists(path)then pcall(fs.move,backup,path)end return false,activateErr end return true end local function volumeFor(path)return disks.forPath(absolute(path))end local function writable(path)local volume=volumeFor(path)return not volume or volume.writable==true end function M.isProtected(path)path=absolute(path)if protectedFiles[path]then return true end for _,root in ipairs(protectedRoots)do if path==root or path:sub(1,#root+1)==root..\"/\"then return true end end for _,volume in ipairs(disks.volumes())do local root=absolute(fs.combine(volume.mount,\".ceetos-trash\"))if path==root or path:sub(1,#root+1)==root..\"/\"then return true end end return false end function M.describe(path)path=absolute(path)local volume=volumeFor(path)return{name=fs.getName(path),path=path,directory=fs.exists(path)and fs.isDir(path)or false,runnable=fs.exists(path)and not fs.isDir(path)and path:sub(-4):lower()==\".lua\",protected=M.isProtected(path),volume=volume and volume.key or nil,removable=volume~=nil,writable=writable(path),}end function M.list(path)path=absolute(path==\"\"and\"/\"or(path or\"/\"))local result={}if path~=\"/\"then local parent=absolute(fs.getDir(path))result[#result+1]={name=\"..\",path=parent,directory=true,parent=true,protected=M.isProtected(parent)}end for _,name in ipairs(fs.list(path))do if name~=\".ceetos-trash\"then result[#result+1]=M.describe(absolute(fs.combine(path,name)))end end if path==\"/\"then local seen={}for _,item in ipairs(result)do seen[item.path]=true end for _,volume in ipairs(disks.volumes())do if not seen[volume.mount]then result[#result+1]={name=volume.label,path=volume.mount,directory=true,removable=true,volume=volume.key,writable=volume.writable,disk=volume}else for _,item in ipairs(result)do if item.path==volume.mount then item.name,item.removable,item.volume,item.writable,item.disk=volume.label,true,volume.key,volume.writable,volume end end end end end return sort(result)end local function collect(path,depth,result)if depth<0 or not fs.exists(path)or not fs.isDir(path)then return end for _,item in ipairs(M.list(path))do if not item.parent then if item.directory then collect(item.path,depth-1,result)elseif item.runnable then result[#result+1]=item end end end end function M.programs()local result={}collect(\"/programs\",2,result)for _,volume in ipairs(disks.volumes())do local before=#result collect(volume.mount,3,result)for index=before+1,#result do result[index].removable,result[index].volume,result[index].disk=true,volume.key,volume result[index].name=volume.label..\": \"..result[index].name end end return result end function M.volumes()return disks.volumes()end function M.handleDiskEvent(event,drive)return disks.handleEvent(event,drive)end function M.runnable(path)return M.describe(path).runnable end function M.path(directory,name)return absolute(fs.combine(directory,fs.getName(name)))end local function assertMutable(path)path=absolute(path)assert(path~=\"/\"and fs.exists(path),\"file not found\")assert(not M.isProtected(path),\"CeetOS system files cannot be changed from Explorer\")assert(writable(path),\"this removable disk is read-only\")return path end function M.rename(path,name)auth.require(\"operator\")path=assertMutable(path)assert(name and fs.getName(name)==name and name~=\"\"and name~=\".\"and name~=\"..\",\"invalid file name\")local target=M.path(fs.getDir(path),name)assert(not M.isProtected(target),\"CeetOS system files cannot be changed from Explorer\")assert(not fs.exists(target),\"a file with that name already exists\")fs.move(path,target)return target end function M.makeDirectory(directory,name)auth.require(\"operator\")directory=absolute(directory)assert(not M.isProtected(directory),\"CeetOS system folders cannot be changed from Explorer\")assert(writable(directory),\"this removable disk is read-only\")assert(name and fs.getName(name)==name and name~=\"\"and name~=\".\"and name~=\"..\",\"invalid folder name\")local target=M.path(directory,name)assert(not fs.exists(target),\"a file with that name already exists\")fs.makeDir(target)return target end function M.readText(path)path=absolute(path)assert(fs.exists(path)and not fs.isDir(path),\"file not found\")local handle=assert(fs.open(path,\"r\"))local contents=handle.readAll()handle.close()return contents end function M.writeText(path,contents)auth.require(\"operator\")path=absolute(path)assert(not M.isProtected(path),\"CeetOS system files cannot be changed from Editor\")assert(writable(path),\"this removable disk is read-only\")local parent=absolute(fs.getDir(path))assert(fs.exists(parent)and fs.isDir(parent),\"parent folder does not exist\")local temporary,backup=path..\".ceetos-tmp\",path..\".ceetos-backup\"if fs.exists(temporary)then fs.delete(temporary)end if fs.exists(backup)then fs.delete(backup)end local handle=assert(fs.open(temporary,\"w\"));handle.write(contents);handle.close()if fs.exists(path)then fs.move(path,backup)end local ok,err=pcall(fs.move,temporary,path)if not ok then if fs.exists(backup)then fs.move(backup,path)end error(err,0)end if fs.exists(backup)then fs.delete(backup)end return path end local function trashRoot(path)local volume=volumeFor(path)if volume then local root=fs.combine(volume.mount,\".ceetos-trash\")return{key=volume.key,root=root,index=fs.combine(root,\"index.lua\"),journal=fs.combine(root,\"journal.lua\"),items=fs.combine(root,\"items\"),volume=volume}end return{key=\"local\",root=TRASH_DIR,index=TRASH_INDEX,journal=TRASH_DIR..\"-journal.lua\",items=TRASH_DIR}end local function envelope(value)if type(value)==\"table\"and value.schema==TRASH_SCHEMA and type(value.entries)==\"table\"then return value end return{schema=TRASH_SCHEMA,revision=0,entries=type(value)==\"table\"and value or{}}end local function trashIndex(ref)local value=ref.key==\"local\"and store.read(ref.index,{})or rawRead(ref.index,{})return envelope(value)end local function saveTrashIndex(ref,index)index.schema,index.revision=TRASH_SCHEMA,(tonumber(index.revision)or 0)+1 if ref.key==\"local\"then return store.write(ref.index,index)end return rawWrite(ref.index,index)end local function commitTrashIndex(ref,index)local ok,err=saveTrashIndex(ref,index)if not ok and ref.key==\"local\"and store.invalidate then store.invalidate(ref.index)end assert(ok,err or\"could not save trash index\")end local function journal(ref)return ref.key==\"local\"and store.read(ref.journal,{})or rawRead(ref.journal,{})end local function saveJournal(ref,value)if ref.key==\"local\"then return store.write(ref.journal,value)end return rawWrite(ref.journal,value)end local function beginTransaction(ref,transaction)local value=journal(ref);value[transaction.id]=transaction local ok,err=saveJournal(ref,value);assert(ok,err or\"could not save trash journal\")end local function finishTransaction(ref,id)local value=journal(ref);value[id]=nil local ok,err=saveJournal(ref,value);assert(ok,err or\"could not finalize trash journal\")end local function trashId(ref)return ref.key..\":\"..tostring(os.epoch(\"utc\"))..\"-\"..tostring(math.random(1000,9999))end local function storedPath(ref,id)local leaf=(tostring(id):gsub(\"[^%w%-%_]\",\"_\"))return absolute(fs.combine(ref.items,leaf))end local function refForId(id)local key=tostring(id or\"\"):match(\"^([^:]+):\")if not key or key==\"local\"then return trashRoot(\"/\")end for _,volume in ipairs(disks.volumes())do if volume.key==key then return trashRoot(volume.mount)end end return nil,\"the source disk is not mounted\"end function M.moveToTrash(path)auth.require(\"operator\")path=assertMutable(path)local ref=trashRoot(path)if not fs.exists(ref.items)then fs.makeDir(ref.items)end local index=trashIndex(ref)local id=trashId(ref)local pending=journal(ref)while index.entries[id]or pending[id]do id=id..\"x\"end local stored=storedPath(ref,id)local entry={original=path,name=fs.getName(path),directory=fs.isDir(path),deletedAt=os.epoch(\"utc\"),volume=ref.key}beginTransaction(ref,{id=id,kind=\"trash\",source=path,stored=stored,entry=entry})fs.move(path,stored)index.entries[id]=entry commitTrashIndex(ref,index)finishTransaction(ref,id)return id end function M.repairTrash()auth.require(\"admin\")local repaired=0 local refs={trashRoot(\"/\")}for _,volume in ipairs(disks.volumes())do refs[#refs+1]=trashRoot(volume.mount)end for _,ref in ipairs(refs)do if ref.key==\"local\"or ref.volume.writable then local index,pending=trashIndex(ref),journal(ref)local refRepaired=0 for id,transaction in pairs(pending)do if transaction.kind==\"trash\"and transaction.entry and fs.exists(transaction.stored)then index.entries[id]=transaction.entry pending[id],refRepaired=nil,refRepaired+1 elseif transaction.kind==\"trash\"and transaction.source and fs.exists(transaction.source)and not fs.exists(transaction.stored)then pending[id],refRepaired=nil,refRepaired+1 elseif transaction.kind==\"restore\"and transaction.target and fs.exists(transaction.target)then index.entries[id]=nil pending[id],refRepaired=nil,refRepaired+1 elseif transaction.kind==\"restore\"and transaction.source and fs.exists(transaction.source)and(not transaction.target or not fs.exists(transaction.target))then pending[id],refRepaired=nil,refRepaired+1 elseif transaction.kind==\"delete\"and(not transaction.source or not fs.exists(transaction.source))then index.entries[id]=nil pending[id],refRepaired=nil,refRepaired+1 elseif transaction.kind==\"delete\"and transaction.source and fs.exists(transaction.source)then pending[id],refRepaired=nil,refRepaired+1 end end if refRepaired>0 then commitTrashIndex(ref,index)local saved,saveErr=saveJournal(ref,pending);assert(saved,saveErr or\"could not finalize trash repair\")repaired=repaired+refRepaired end end end return repaired end function M.trashList()local refs,result={trashRoot(\"/\")},{}for _,volume in ipairs(disks.volumes())do refs[#refs+1]=trashRoot(volume.mount)end for _,ref in ipairs(refs)do local index=trashIndex(ref)for id,entry in pairs(index.entries)do local stored=storedPath(ref,id)if fs.exists(stored)then result[#result+1]={id=id,path=stored,name=entry.name,original=entry.original,directory=entry.directory,trashed=true,deletedAt=entry.deletedAt,runnable=not entry.directory and entry.name:sub(-4):lower()==\".lua\",removable=ref.key~=\"local\",volume=ref.key}end end for id,pending in pairs(journal(ref))do if pending.kind==\"trash\"and pending.entry and fs.exists(pending.stored)and not index.entries[id]then local entry=pending.entry result[#result+1]={id=id,path=pending.stored,name=entry.name,original=entry.original,directory=entry.directory,trashed=true,pending=true,recovery=\"move\",deletedAt=entry.deletedAt,runnable=not entry.directory and entry.name:sub(-4):lower()==\".lua\",removable=ref.key~=\"local\",volume=ref.key}elseif pending.kind==\"restore\"and pending.entry then local entry=pending.entry result[#result+1]={id=id,path=pending.target or pending.source,name=entry.name,original=entry.original,directory=entry.directory,trashed=true,pending=true,recovery=\"restore\",deletedAt=entry.deletedAt,runnable=not entry.directory and entry.name:sub(-4):lower()==\".lua\",removable=ref.key~=\"local\",volume=ref.key}elseif pending.kind==\"delete\"and pending.entry then local entry=pending.entry result[#result+1]={id=id,path=pending.source,name=entry.name,original=entry.original,directory=entry.directory,trashed=true,pending=true,recovery=\"delete\",deletedAt=entry.deletedAt,runnable=not entry.directory and entry.name:sub(-4):lower()==\".lua\",removable=ref.key~=\"local\",volume=ref.key}end end end table.sort(result,function(a,b)return(a.deletedAt or 0)>(b.deletedAt or 0)end)return result end function M.restore(id)auth.require(\"operator\")local ref,refErr=refForId(id);assert(ref,refErr)local index=trashIndex(ref)local entry=index.entries[id]assert(entry,\"trash item not found\")local stored=storedPath(ref,id)assert(fs.exists(stored),\"trash item no longer exists\")local parent=absolute(fs.getDir(entry.original))assert(not M.isProtected(entry.original),\"cannot restore into a protected system path\")assert(writable(entry.original),\"this removable disk is read-only\")if not fs.exists(parent)then fs.makeDir(parent)end local target=availablePath(parent,fs.getName(entry.original))beginTransaction(ref,{id=id,kind=\"restore\",source=stored,target=target,entry=entry})fs.move(stored,target)index.entries[id]=nil commitTrashIndex(ref,index)finishTransaction(ref,id)return target end function M.deleteTrash(id)auth.require(\"operator\")local ref,refErr=refForId(id);assert(ref,refErr)local index,entry=trashIndex(ref),trashIndex(ref).entries[id]assert(entry,\"trash item not found\")local stored=storedPath(ref,id)beginTransaction(ref,{id=id,kind=\"delete\",source=stored,entry=entry})if fs.exists(stored)then fs.delete(stored)end index.entries[id]=nil commitTrashIndex(ref,index)finishTransaction(ref,id)end function M.emptyTrash()auth.require(\"operator\")local refs={trashRoot(\"/\")}for _,volume in ipairs(disks.volumes())do refs[#refs+1]=trashRoot(volume.mount)end for _,ref in ipairs(refs)do if ref.key==\"local\"or ref.volume.writable then local ids={}for id in pairs(trashIndex(ref).entries)do ids[#ids+1]=id end for _,id in ipairs(ids)do M.deleteTrash(id)end end end end function M.import(transfers,directory)auth.require(\"operator\")directory=absolute(directory or\"/programs/imports\")assert(not M.isProtected(directory),\"cannot import into a protected system path\")assert(writable(directory),\"this removable disk is read-only\")if not fs.exists(directory)then fs.makeDir(directory)end assert(type(transfers)==\"table\"and#transfers<=IMPORT_MAX_FILES,\"too many files to import\")local imported,total={},0 for _,transfer in ipairs(transfers or{})do local name=type(transfer)==\"table\"and transfer.getName and transfer.getName()assert(type(name)==\"string\"and name~=\"\"and name~=\".\"and name~=\"..\"and fs.getName(name)==name,\"invalid imported filename\")if name and name~=\"\"then local path=availablePath(directory,name)assert(not M.isProtected(path),\"cannot import into a protected system path\")local staged=path..\".ceetos-import-new\"if fs.exists(staged)then fs.delete(staged)end local handle=assert(fs.open(staged,\"wb\"))local size,chunks,complete=0,0,false local ok,err=pcall(function()while true do local chunk=transfer.read(8192)if chunk==nil then complete=true;break end assert(type(chunk)==\"string\",\"invalid file transfer chunk\")chunks=chunks+1 assert(#chunk>0 and chunks<=IMPORT_MAX_CHUNKS,\"invalid or excessive file transfer chunks\")size,total=size+#chunk,total+#chunk assert(size<=IMPORT_MAX_FILE_BYTES,\"imported file exceeds 1 MiB limit\")assert(total<=IMPORT_MAX_TOTAL_BYTES,\"import batch exceeds 8 MiB limit\")handle.write(chunk)end end)handle.close()if not ok or not complete then if fs.exists(staged)then fs.delete(staged)end error(err or\"incomplete file transfer\",0)end local activated,activateErr=pcall(fs.move,staged,path)if not activated then if fs.exists(staged)then fs.delete(staged)end;error(activateErr,0)end imported[#imported+1]=path end end return imported end return M",
  ["ceetos/lib/disks.lua"] = "local store=require(\"ceetos.lib.store\")local M={}local cached,dirty={},true local MAX_VOLUMES=32 local MOUNT_STATE=\"/ceetos/data/disk-volumes.lua\"local mountState local function generations()if mountState then return mountState end mountState=store.readFresh(MOUNT_STATE,{schema=1,generations={}})if type(mountState)~=\"table\"then mountState={schema=1,generations={}}end if type(mountState.generations)~=\"table\"then mountState.generations={}end return mountState end local function mountKey(id,drive)return(id~=nil and\"disk-\"..tostring(id)or\"drive-\"..tostring(drive)):gsub(\"[^%w%-%_]\",\"_\")end local function bumpGeneration(key)local value=generations()value.generations[key]=math.max(0,math.floor(tonumber(value.generations[key])or 0))+1 store.write(MOUNT_STATE,value)return value.generations[key]end local function absolute(path)if type(path)~=\"string\"or path==\"\"then return nil end return path:sub(1,1)==\"/\"and path or\"/\"..path end local function safe(fn,...)if type(fn)~=\"function\"then return nil end local ok,value=pcall(fn,...)if ok then return value end return nil end local function clean(value,limit)value=tostring(value or\"\")value=value:gsub(\"[%c]\",\" \")return value:sub(1,limit or 96)end local function driveNames()local names,result=peripheral and peripheral.getNames and peripheral.getNames()or{},{}for _,name in ipairs(names or{})do if peripheral.getType(name)==\"drive\"then result[#result+1]=name end end table.sort(result)return result end function M.refresh()local result={}if not disk then cached,dirty=result,false;return cached end for _,drive in ipairs(driveNames())do if#result>=MAX_VOLUMES then break end local present=safe(disk.isPresent,drive)local hasData=present and safe(disk.hasData,drive)local mount=hasData and absolute(safe(disk.getMountPath,drive))or nil if mount and fs.exists(mount)and fs.isDir(mount)then local id=safe(disk.getID,drive)local label=clean(safe(disk.getLabel,drive),48)local readonly=safe(fs.isReadOnly,mount)==true local key=mountKey(id,drive)local state=generations()if state.generations[key]==nil then state.generations[key]=1;store.write(MOUNT_STATE,state)end result[#result+1]={drive=tostring(drive),id=id~=nil and tostring(id)or nil,key=key,token=key..\":\"..tostring(state.generations[key]),label=label~=\"\"and label or(\"Disk \"..tostring(id or drive)),mount=mount,available=true,writable=not readonly,}end end table.sort(result,function(a,b)return a.label:lower()<b.label:lower()end)cached,dirty=result,false return cached end function M.volumes()if dirty then M.refresh()end local copy={}for index,item in ipairs(cached)do copy[index]={}for key,value in pairs(item)do copy[index][key]=value end end return copy end function M.invalidate()dirty=true end function M.forPath(path)path=absolute(path)if not path then return nil end for _,volume in ipairs(M.volumes())do if path==volume.mount or path:sub(1,#volume.mount+1)==volume.mount..\"/\"then return volume end end return nil end function M.isMounted(volume)if type(volume)~=\"table\"or type(volume.mount)~=\"string\"then return false end for _,candidate in ipairs(M.volumes())do if candidate.key==volume.key and candidate.mount==volume.mount then return true end end return false end function M.handleEvent(event,drive)if event==\"disk\"or event==\"disk_eject\"or event==\"peripheral\"or event==\"peripheral_detach\"then if(event==\"disk\"or event==\"disk_eject\")and drive then if event==\"disk\"then M.refresh()end for _,volume in ipairs(cached)do if volume.drive==tostring(drive)then bumpGeneration(volume.key)end end end M.invalidate()return true end return false end return M",
  ["ceetos/lib/editor.lua"] = "local M={}local selection=require(\"ceetos.lib.selection\")local function clamp(value,low,high)return math.max(low,math.min(high,value))end local function split(text)text=(text or\"\"):gsub(\"\\r\\n\",\"\\n\"):gsub(\"\\r\",\"\\n\")local lines={}for line in(text..\"\\n\"):gmatch(\"(.-)\\n\")do lines[#lines+1]=line end if#lines==0 then lines[1]=\"\"end return lines end function M.new(text)local lines=split(text)return{lines=lines,line=1,column=0,changed=false,preferredColumn=nil,selection=selection.new(lines)}end function M.text(buffer)return table.concat(buffer.lines,\"\\n\")end function M.current(buffer)return buffer.lines[buffer.line]end function M.clampCursor(buffer)buffer.line=clamp(buffer.line,1,#buffer.lines)buffer.column=clamp(buffer.column,0,#buffer.lines[buffer.line])end function M.move(buffer,lineDelta,columnDelta)buffer.line=clamp(buffer.line+(lineDelta or 0),1,#buffer.lines)buffer.column=clamp((buffer.preferredColumn or buffer.column)+(columnDelta or 0),0,#buffer.lines[buffer.line])if lineDelta and lineDelta~=0 and columnDelta==0 then buffer.column=clamp(buffer.preferredColumn or buffer.column,0,#buffer.lines[buffer.line])end buffer.preferredColumn=buffer.column end function M.gotoLine(buffer,line,column)buffer.line=clamp(tonumber(line)or 1,1,#buffer.lines)buffer.column=clamp(tonumber(column)or 0,0,#buffer.lines[buffer.line])buffer.preferredColumn=buffer.column end function M.selectStart(buffer)buffer.selection.lines=buffer.lines;selection.start(buffer.selection,buffer.line,buffer.column);return buffer end function M.selectTo(buffer,line,column)buffer.selection.lines=buffer.lines;selection.update(buffer.selection,line,column);return buffer end function M.clearSelection(buffer)selection.clear(buffer.selection);return buffer end function M.selectionText(buffer)buffer.selection.lines=buffer.lines;return selection.text(buffer.selection)end function M.copySelection(buffer,clipboard)buffer.selection.lines=buffer.lines;return selection.copy(buffer.selection,clipboard)end function M.deleteSelection(buffer)local range=selection.range(buffer.selection);if not range then return false end local first,last=range.first,range.last local startText=(buffer.lines[first.line]or\"\"):sub(1,first.column)local endText=(buffer.lines[last.line]or\"\"):sub(last.column+1)buffer.lines[first.line]=startText..endText for _=first.line+1,last.line do table.remove(buffer.lines,first.line+1)end buffer.line,buffer.column,buffer.preferredColumn=first.line,first.column,first.column buffer.selection.lines=buffer.lines;selection.clear(buffer.selection);buffer.changed=true;return true end function M.insert(buffer,value)value=value or\"\"if selection.range(buffer.selection)then M.deleteSelection(buffer)end local before,after=M.current(buffer):sub(1,buffer.column),M.current(buffer):sub(buffer.column+1)local parts=split(value)if#parts==1 then buffer.lines[buffer.line]=before..value..after buffer.column=buffer.column+#value else buffer.lines[buffer.line]=before..parts[1]for index=2,#parts-1 do table.insert(buffer.lines,buffer.line+index-1,parts[index])end table.insert(buffer.lines,buffer.line+#parts-1,parts[#parts]..after)buffer.line,buffer.column=buffer.line+#parts-1,#parts[#parts]end buffer.changed,buffer.preferredColumn=true,buffer.column end function M.backspace(buffer)if buffer.column>0 then local line=M.current(buffer)buffer.lines[buffer.line]=line:sub(1,buffer.column-1)..line:sub(buffer.column+1)buffer.column=buffer.column-1 elseif buffer.line>1 then local previous=buffer.lines[buffer.line-1]buffer.lines[buffer.line-1]=previous..M.current(buffer)table.remove(buffer.lines,buffer.line);buffer.line,buffer.column=buffer.line-1,#previous else return false end buffer.changed,buffer.preferredColumn=true,buffer.column;return true end function M.delete(buffer)local line=M.current(buffer)if buffer.column<#line then buffer.lines[buffer.line]=line:sub(1,buffer.column)..line:sub(buffer.column+2)elseif buffer.line<#buffer.lines then buffer.lines[buffer.line]=line..buffer.lines[buffer.line+1];table.remove(buffer.lines,buffer.line+1)else return false end buffer.changed=true;return true end function M.find(buffer,needle,startLine)if not needle or needle==\"\"then return nil end for line=startLine or buffer.line,#buffer.lines do local column=buffer.lines[line]:find(needle,line==buffer.line and buffer.column+1 or 1,true)if column then buffer.line,buffer.column,buffer.preferredColumn=line,column-1,column-1;return line,column-1 end end return nil end local keywords={[\"and\"]=true,[\"break\"]=true,[\"do\"]=true,[\"else\"]=true,[\"elseif\"]=true,[\"end\"]=true,[\"false\"]=true,[\"for\"]=true,[\"function\"]=true,[\"if\"]=true,[\"in\"]=true,[\"local\"]=true,[\"nil\"]=true,[\"not\"]=true,[\"or\"]=true,[\"repeat\"]=true,[\"return\"]=true,[\"then\"]=true,[\"true\"]=true,[\"until\"]=true,[\"while\"]=true}local builtins={print=true,pairs=true,ipairs=true,tonumber=true,tostring=true,type=true,error=true,assert=true,fs=true,term=true,shell=true,peripheral=true,os=true,textutils=true}function M.tokens(line,lua)if not lua then return{{text=line,kind=\"text\"}}end local out,index={},1 local function add(text,kind)if text~=\"\"then out[#out+1]={text=text,kind=kind}end end while index<=#line do local rest=line:sub(index)if rest:sub(1,2)==\"--\"then add(rest,\"comment\");break end local quote=rest:sub(1,1)if quote==\"'\"or quote=='\"'then local finish,escaped=2,false while finish<=#rest do if rest:sub(finish,finish)==quote and not escaped then break end;escaped=rest:sub(finish,finish)==\"\\\\\"and not escaped;if rest:sub(finish,finish)~=\"\\\\\"then escaped=false end;finish=finish+1 end add(rest:sub(1,finish),\"string\");index=index+finish else local word=rest:match(\"^[%a_][%w_]*\")local number=rest:match(\"^%d+%.?%d*\")if word then add(word,keywords[word]and\"keyword\"or(builtins[word]and\"builtin\"or\"text\"));index=index+#word elseif number then add(number,\"number\");index=index+#number else add(rest:sub(1,1),\"text\");index=index+1 end end end return out end return M",
  ["ceetos/lib/terminal.lua"] = "local store=require(\"ceetos.lib.store\")local M,PATH,LIMIT={},\"/ceetos/data/terminal-history.lua\",50 function M.history()return store.read(PATH,{})end function M.add(command)if not command or command==\"\"then return end local history=M.history()if history[#history]~=command then history[#history+1]=command end while#history>LIMIT do table.remove(history,1)end store.write(PATH,history)end function M.complete(prefix,candidates)local result,seen={},{}for _,candidate in ipairs(candidates or{})do if candidate:sub(1,#prefix)==prefix and not seen[candidate]then result[#result+1],seen[candidate]=candidate,true end end table.sort(result);return result end return M",
  ["ceetos/lib/selection.lua"] = "local M={}local function point(line,column)return{line=math.max(1,tonumber(line)or 1),column=math.max(0,tonumber(column)or 0)}end local function before(a,b)return a.line<b.line or(a.line==b.line and a.column<=b.column)end local function norm(a,b)return before(a,b)and a or b,before(a,b)and b or a end function M.new(lines)return{lines=lines or{\"\"},anchor=nil,active=nil}end function M.position(line,column,maxLine,maxColumn)line=math.max(1,math.min(tonumber(maxLine)or line or 1,tonumber(line)or 1))column=math.max(0,tonumber(column)or 0)if maxColumn and line==maxLine then column=math.min(column,maxColumn)end return line,column end function M.start(s,line,column)s.anchor,s.active=point(line,column),point(line,column);return s end function M.update(s,line,column)if s.anchor then s.active=point(line,column)end;return s end function M.clear(s)s.anchor,s.active=nil,nil;return s end function M.range(s)if not s.anchor or not s.active then return nil end local first,last=norm(s.anchor,s.active)if first.line==last.line and first.column==last.column then return nil end return{first=first,last=last}end function M.text(s)local r=M.range(s);if not r then return\"\"end local out={}for line=r.first.line,r.last.line do local value=s.lines[line]or\"\"local from=line==r.first.line and r.first.column+1 or 1 local to=line==r.last.line and r.last.column or#value out[#out+1]=value:sub(from,to)end return table.concat(out,\"\\n\")end function M.copy(s,clipboard)local text=M.text(s);if text~=\"\"and clipboard then clipboard.set(text)end;return text end return M",
  ["ceetos/lib/clipboard.lua"] = "local store=require(\"ceetos.lib.store\")local M,PATH,OUTBOX,LIMIT={},\"/ceetos/data/clipboard.lua\",\"/ceetos-dev/clipboard-out.lua\",16384 local memory local function trim(value)return tostring(value or\"\"):sub(1,LIMIT)end function M.get()if memory~=nil then return memory end local value=store.read(PATH,{text=\"\"})memory=value and value.text or\"\"return memory end function M.set(value)local text=trim(value)memory=text pcall(store.write,PATH,{text=text})if fs and fs.makeDir and fs.open then pcall(function()if not fs.exists(\"/ceetos-dev\")then pcall(fs.makeDir,\"/ceetos-dev\")end local handle=fs.open(OUTBOX,\"w\")if handle then handle.write(textutils.serialize({text=text,queued=os.epoch and os.epoch(\"utc\")or 0}));handle.close()end end)end return text end function M.clear()return M.set(\"\")end function M.copy(value)return M.set(value)end function M.limit()return LIMIT end return M",
  ["ceetos/lib/input.lua"] = "local M={}function M.modifiers()return{ctrl=false,shift=false}end local function nameOf(name)name=tostring(name or\"\")local lower=name:lower()if lower==\"leftctrl\"or lower==\"rightctrl\"or lower==\"ctrl\"then return\"ctrl\"end if lower==\"leftshift\"or lower==\"rightshift\"or lower==\"shift\"then return\"shift\"end return lower end function M.key(state,name)name=nameOf(name)if name==\"ctrl\"then state.ctrl=true elseif name==\"shift\"then state.shift=true end return state end function M.keyUp(state,name)name=nameOf(name)if name==\"ctrl\"then state.ctrl=false elseif name==\"shift\"then state.shift=false end return state end function M.shortcut(state,name)name=nameOf(name)local active=state.ctrl and state.shift and(name==\"c\"or name==\"v\")if active then state.ctrl,state.shift=false,false end return active,name end function M.navigation(name)name=nameOf(name)return({w=\"up\",a=\"left\",s=\"down\",d=\"right\"})[name]or name end return M",
  ["ceetos/lib/modal_input.lua"] = "local M={}local function keyName(code)if type(keys)==\"table\"and type(keys.getName)==\"function\"then return tostring(keys.getName(code)or\"\"):lower()end return tostring(code or\"\"):lower()end local function clean(value)return tostring(value or\"\"):gsub(\"[\\r\\n]\",\" \")end function M.read(options)options=type(options)==\"table\"and options or{}local value=clean(options.initial)local cursor=math.max(0,math.min(#value,tonumber(options.cursor)or#value))local pull=options.pull or os.pullEventRaw assert(type(pull)==\"function\",\"modal input requires an event source\")assert(type(options.draw)==\"function\",\"modal input requires a draw function\")local function redraw()options.draw(value,cursor,options.secret==true)end local function insert(text)text=clean(text)if text==\"\"then return end local limit=math.max(1,math.floor(tonumber(options.limit)or 160))local room=limit-#value if room<=0 then return end text=text:sub(1,room)value=value:sub(1,cursor)..text..value:sub(cursor+1)cursor=cursor+#text end redraw()while true do local event,first=pull()if event==\"char\"or event==\"paste\"then insert(first)redraw()elseif event==\"key\"then local name=keyName(first)if name==\"enter\"then return value,true elseif name==\"backspace\"then if cursor<=0 and value==\"\"then return nil,false end if cursor>0 then value=value:sub(1,cursor-1)..value:sub(cursor+1)cursor=cursor-1 end elseif name==\"delete\"then value=value:sub(1,cursor)..value:sub(cursor+2)elseif name==\"left\"then cursor=math.max(0,cursor-1)elseif name==\"right\"then cursor=math.min(#value,cursor+1)elseif name==\"home\"then cursor=0 elseif name==\"end\"then cursor=#value else goto continue end redraw()elseif event==\"term_resize\"then redraw()elseif event==\"terminate\"then return nil,false end::continue::end end return M",
  ["ceetos/lib/foreground_task.lua"] = "local M={}local unpackValues=table.unpack or unpack function M.start(fn,...)assert(type(fn)==\"function\",\"foreground task requires a function\")local args={...}local task={co=coroutine.create(function()return fn(unpackValues(args))end),filter=nil,done=false}M.step(task)return task end function M.step(task,event,...)if type(task)~=\"table\"or task.done then return false,true end if task.filter~=nil and event~=task.filter then return false,false end local result={coroutine.resume(task.co,event,...)}if result[1]~=true then task.done,task.ok,task.error=true,false,tostring(result[2]or\"foreground task failed\")return true,true end if coroutine.status(task.co)==\"dead\"then task.done,task.ok=true,result[2]==true task.result,task.error=result[2],result[3]return true,true end task.filter=result[2]return true,false end function M.result(task)if type(task)~=\"table\"or not task.done then return nil,\"foreground task is still running\"end return task.result,task.error end return M",
  ["ceetos/lib/pairing_monitor.lua"] = "local M={}function M.begin(detail,now,maximumWait)detail=type(detail)==\"table\"and detail or{}local peer=tostring(detail.peer or\"\")if peer==\"\"then return nil,\"Pairing request was sent; waiting for confirmation\"end now=tonumber(now)or 0 maximumWait=math.max(1000,tonumber(maximumWait)or 120000)local expiry=tonumber(detail.expires)or(now+maximumWait)return{peer=peer,label=tostring(detail.label or(\"Computer \"..peer)),deadline=math.min(expiry,now+maximumWait),},nil end function M.poll(pending,snapshot,now)if type(pending)~=\"table\"then return false end if type(snapshot)==\"table\"then for _,peer in ipairs(snapshot.peers or{})do if tostring(peer.id)==tostring(pending.peer)and not peer.rekeyRequired then return true,\"Paired with \"..tostring(peer.label or pending.label),false end end local discovery=snapshot.discovery or{}if discovery.active==false and discovery.lastState==\"paired\"then return true,\"Paired with \"..tostring(pending.label or pending.peer),false end if discovery.active==false and discovery.lastState and discovery.lastState~=\"paired\"and discovery.lastState~=\"inactive\"then return true,\"Pairing did not complete: \"..tostring(discovery.lastMessage or discovery.lastState),true end end if(tonumber(now)or 0)>=(tonumber(pending.deadline)or 0)then return true,\"Pairing timed out before \"..tostring(pending.label or pending.peer)..\" confirmed. Check Peers diagnostics.\",true end return false end return M",
  ["ceetos/lib/version.lua"] = "return{major=0,minor=19,patch=50,string=\"0.19.50\"}",
  ["ceetos/lib/crafting.lua"] = "local M={}M.RECIPE_PACK_MAX_BYTES=64*1024 function M.isLocalPeer(peer,computerId)return tostring(peer)==tostring(computerId)end function M.normalise(rows)local out={}for _,row in pairs(type(rows)==\"table\"and rows or{})do if type(row)==\"table\"then local name,count=row.name or row.id,tonumber(row.count or row.amount or row.quantity or 0)or 0 if type(name)==\"string\"and count>0 then out[name]=(out[name]or 0)+math.floor(count)end end end return out end function M.recipeStockSnapshot(rows,maximum)maximum=math.max(1,math.min(512,math.floor(tonumber(maximum)or 512)))local counts={}for _,row in pairs(type(rows)==\"table\"and rows or{})do if type(row)==\"table\"then local name=row.name or row.id local count=tonumber(row.count or row.amount or row.quantity or 0)if type(name)==\"string\"and name:match(\"^[%w_.:%-]+$\")and count and count==count and count~=math.huge and count~=-math.huge and count>0 then counts[name]=math.min(2147483647,(counts[name]or 0)+math.floor(count))end end end local names,snapshot={},{}for name in pairs(counts)do names[#names+1]=name end table.sort(names)for index=1,math.min(#names,maximum)do local name=names[index]snapshot[#snapshot+1]={name=name,count=counts[name]}end return snapshot end function M.readTickerStock(ticker,maximum)if type(ticker)~=\"table\"or type(ticker.stock)~=\"function\"then return nil,\"Selected Stock Ticker does not expose stock()\"end local ok,rows=pcall(ticker.stock)if not ok then return nil,tostring(rows)end if type(rows)~=\"table\"then return nil,\"Stock Ticker returned invalid stock data\"end return M.recipeStockSnapshot(rows,maximum)end function M.shouldRefreshStock(state)return type(state)==\"table\"and(state.stockRefreshRequested==true or type(state.active)==\"table\")or false end local RECOVERABLE_RECIPE_ERRORS={[\"peer is unreachable, degraded, or requires rekey\"]=true,[\"could not reach recipe server\"]=true,[\"recipe server did not respond to catalogue request\"]=true,[\"recipe server rejected catalogue request\"]=true,[\"recipe database did not respond\"]=true,}function M.clearRecoveredRecipeError(state)if type(state)~=\"table\"or type(state.active)==\"table\"or type(state.error)~=\"string\"then return false end local key=state.error:lower():gsub(\"^%s+\",\"\"):gsub(\"%s+$\",\"\")if not RECOVERABLE_RECIPE_ERRORS[key]then return false end state.error=nil return true end function M.persistenceAction(lastEncoded,durableEncoded,now,lastVerifiedAt,interval)now=math.max(0,math.floor(tonumber(now)or 0))lastVerifiedAt=math.max(0,math.floor(tonumber(lastVerifiedAt)or 0))interval=math.max(250,math.min(60000,math.floor(tonumber(interval)or 5000)))if type(lastEncoded)~=\"string\"then return\"write\"end if now<lastVerifiedAt+interval then return\"skip\"end if type(durableEncoded)~=\"string\"then return\"verify\"end return durableEncoded==lastEncoded and\"skip\"or\"write\"end local TRANSIENT_QUEUE_STATE={recipes=true,recipeTags=true,recipeOverlay=true,sources=true,stockRequest=true,catalogRequest=true,tagOptionsRequest=true,commandLedger=true,}function M.durableQueueSnapshot(state)local snapshot={}for key,value in pairs(type(state)==\"table\"and state or{})do if not TRANSIENT_QUEUE_STATE[key]then snapshot[key]=value end end return snapshot end function M.pruneReplyMailbox(replies,maximum)if type(replies)~=\"table\"then return{}end maximum=math.max(1,math.min(256,math.floor(tonumber(maximum)or 64)))local rows={}for requestId,reply in pairs(replies)do if type(requestId)==\"string\"and type(reply)==\"table\"then rows[#rows+1]={id=requestId,received=math.floor(tonumber(reply.received)or 0)}else replies[requestId]=nil end end table.sort(rows,function(left,right)if left.received~=right.received then return left.received<right.received end return left.id<right.id end)while#rows>maximum do replies[table.remove(rows,1).id]=nil end return replies end function M.commandCreatedAt(command)local value=type(command)==\"table\"and tonumber(command.createdAt)or nil if value and value==value and value>0 and value<10000000000000000 then return math.floor(value)end local token=type(command)==\"table\"and command.token or nil if type(token)~=\"string\"then return nil end local encoded=token:match(\"^craft%-ui%-(%d+)%-\")or token:match(\"^dev%-e2e%-(%d+)%-\")value=tonumber(encoded)if value and value==value and value>0 and value<10000000000000000 then return math.floor(value)end return nil end function M.commandIsStale(command,runtimeStartedAt)local started=tonumber(runtimeStartedAt)if not started or started<=0 then return false end local created=M.commandCreatedAt(command)return not created or created<math.floor(started)end function M.displayName(name)name=tostring(name or\"\")return name:match(\"^[^:]+:(.+)$\")or name end function M.formatCount(count)count=math.max(0,math.floor(tonumber(count)or 0))if count>=10000000 then return tostring(math.floor(count/1000000))..\"M\"end if count>=10000 then return tostring(math.floor(count/1000))..\"k\"end return tostring(count)end local function catalogMap(catalog)local out,input={},type(catalog)==\"table\"and(catalog.rows or catalog)or{}for _,row in ipairs(input)do if type(row)==\"table\"and type(row.name)==\"string\"and row.name:match(\"^[%w_.:%-]+$\")and row.name:sub(1,1)~=\"#\"then local tags={}for _,tag in ipairs(type(row.tags)==\"table\"and row.tags or{})do if type(tag)==\"string\"and tag:match(\"^#[%w_.:%-]+$\")then tags[#tags+1]=tag end end table.sort(tags)out[row.name]={tags=tags}end end return out end function M.rows(stock,query,filter,recipes,catalog)local output,needle,available={},tostring(query or\"\"):lower(),M.normalise(stock)local names,remote={},catalogMap(catalog)for name in pairs(available)do names[name]=true end for name in pairs(type(recipes)==\"table\"and recipes or{})do names[name]=true end for name in pairs(remote)do names[name]=true end for name in pairs(names)do local count=available[name]or 0 local craftable=(type(recipes)==\"table\"and recipes[name]~=nil)or remote[name]~=nil local display=M.displayName(name):lower()local include=needle==\"\"or name:lower():find(needle,1,true)or display:find(needle,1,true)if filter==\"craftable\"or filter==\"recipes\"then include=include and craftable elseif filter==\"stocked\"then include=include and count>0 end local recipeTags=remote[name]and remote[name].tags or((type(recipes)==\"table\"and recipes[name]and M.recipeTagReferences(recipes[name]))or{})if include then output[#output+1]={name=name,count=count,craftable=craftable,stocked=count>0,tags=recipeTags}end end table.sort(output,function(a,b)if a.craftable~=b.craftable then return a.craftable end return a.name<b.name end)return output end function M.addToCart(cart,name,count)if type(cart)~=\"table\"or type(name)~=\"string\"or name==\"\"then return nil,\"invalid cart item\"end count=tonumber(count)if not count or count<1 then return nil,\"invalid cart quantity\"end count=math.floor(count)for _,row in ipairs(cart)do if row.name==name then row.count=count;return row,\"updated\"end end local row={name=name,count=count}cart[#cart+1]=row return row,\"added\"end function M.cartTotals(cart)local items,quantity=0,0 for _,row in ipairs(type(cart)==\"table\"and cart or{})do items=items+1 quantity=quantity+math.max(0,math.floor(tonumber(row.count)or 0))end return items,quantity end function M.requesterCheckpointDue(pending,nextCheckpoint,now)pending=math.max(0,math.floor(tonumber(pending)or 0))if pending==0 then return true end return(tonumber(now)or 0)>=(tonumber(nextCheckpoint)or 0)end function M.acceptedCount(result,requested)local accepted=tonumber(result)requested=tonumber(requested)if not accepted or not requested or requested<1 then return nil,\"invalid request result\"end accepted=math.floor(accepted)if accepted<1 then return nil,\"Stock Ticker accepted no items\"end return math.min(accepted,math.floor(requested))end function M.confirmRequesterBatch(baseline,observed,output)baseline,observed,output=tonumber(baseline),tonumber(observed),tonumber(output)if not baseline or not observed or not output or output<1 or output~=math.floor(output)then return nil,\"invalid requester batch progress\"end return observed>=baseline+output end function M.requesterStepProgress(batches,issued,output)batches,issued,output=tonumber(batches),tonumber(issued),tonumber(output)if not batches or not issued or not output or batches<1 or issued<0 or issued>batches or output<1 then return nil,\"invalid requester step progress\"end batches,issued,output=math.floor(batches),math.floor(issued),math.floor(output)return{remaining=batches-issued,expectedOutput=issued*output}end function M.requesterBatchBaseline(stock,outputName)if type(outputName)~=\"string\"or outputName==\"\"then return nil,\"invalid requester output\"end return M.normalise(stock)[outputName]or 0 end function M.displayRow(name,count,width)name=M.displayName(name)local suffix=\" x\"..M.formatCount(count)width=math.max(#suffix+1,math.floor(tonumber(width)or#suffix+1))if#name+#suffix<=width then return name..suffix end return name:sub(1,math.max(1,width-#suffix-3))..\"...\"..suffix end function M.cancellationStatus(issued)return issued and\"cancelled-unconfirmed\"or\"cancelled\"end local function validReference(name)return type(name)==\"string\"and name:match(\"^#?[%w_.:%-]+$\")~=nil and not name:find(\"##\",1,true)end local function recipeIngredients(recipe,batches)local combined={}local declared=type(recipe.ingredients)==\"table\"and#recipe.ingredients>0 if declared then for _,ingredient in ipairs(recipe.ingredients)do local name=type(ingredient)==\"table\"and ingredient.id or ingredient local count=type(ingredient)==\"table\"and tonumber(ingredient.count or 1)or 1 if validReference(name)then combined[name]=(combined[name]or 0)+batches*math.max(1,math.floor(count or 1))end end else for slot=1,9 do local name=recipe.grid and recipe.grid[slot]if name and name~=false then combined[name]=(combined[name]or 0)+batches end end end local out={}for name,count in pairs(combined)do out[#out+1]={name=name,count=count}end table.sort(out,function(a,b)return a.name<b.name end)return out end function M.recipeInputs(recipe,batches)return recipeIngredients(recipe or{},math.max(1,math.floor(tonumber(batches)or 1)))end function M.validateRecipes(recipes)if type(recipes)~=\"table\"then return nil,\"recipes must return a table\"end for name,recipe in pairs(recipes)do if type(name)~=\"string\"or not name:match(\"^[%w_.:%-]+$\")or type(recipe)~=\"table\"then return nil,\"invalid recipe item id\"end if type(recipe.alternatives)==\"table\"then if#recipe.alternatives<1 then return nil,\"recipe \"..name..\" has no alternatives\"end for _,alternative in ipairs(recipe.alternatives)do local valid,err=M.validateRecipes({[name]=alternative});if not valid then return nil,err end end else if recipe.mode~=\"crafting\"then return nil,\"recipe \"..name..\" must use mode = crafting\"end if type(recipe.address)~=\"string\"or recipe.address==\"\"or#recipe.address>64 then return nil,\"recipe \"..name..\" requires an address\"end local crafter=recipe.address==\"Crafter\"if crafter and type(recipe.grid)~=\"table\"then return nil,\"recipe \"..name..\" requires a 9-slot grid for Crafter\"end if not crafter and type(recipe.ingredients)~=\"table\"and type(recipe.grid)~=\"table\"then return nil,\"recipe \"..name..\" requires ingredients\"end local output=tonumber(recipe.output or 1)if not output or output<1 or output~=math.floor(output)then return nil,\"invalid output for \"..name end for slot=1,9 do local value=recipe.grid and recipe.grid[slot]if value~=nil and value~=false and not validReference(value)then return nil,\"invalid grid slot in \"..name end end for _,ingredient in ipairs(type(recipe.ingredients)==\"table\"and recipe.ingredients or{})do local id,count=type(ingredient)==\"table\"and ingredient.id or ingredient,type(ingredient)==\"table\"and ingredient.count or 1 if not validReference(id)or not tonumber(count)or tonumber(count)<1 then return nil,\"invalid ingredient in \"..name end end end end return recipes end local function dataParser(source,maximum)if type(source)~=\"string\"then return nil,\"recipe data must be text\"end if#source==0 or#source>(maximum or M.RECIPE_PACK_MAX_BYTES)then return nil,\"recipe data exceeds the 64 KiB import limit\"end local state={source=source,index=1,length=#source,depth=0,nodes=0}local function skip()while state.index<=state.length do local char=state.source:sub(state.index,state.index)if char:match(\"%s\")then state.index=state.index+1 elseif state.source:sub(state.index,state.index+1)==\"--\"then local ending=state.source:find(\"\\n\",state.index+2,true)state.index=ending and ending+1 or state.length+1 else return true end end return true end local function fail(message)return nil,message..\" near byte \"..tostring(state.index)end local function identifier()local name=state.source:match(\"^[%a_][%w_]*\",state.index)if name then state.index=state.index+#name end return name end local parseValue local function parseString()local quote=state.source:sub(state.index,state.index);state.index=state.index+1 local out={}while state.index<=state.length do local char=state.source:sub(state.index,state.index);state.index=state.index+1 if char==quote then return table.concat(out)end if char==\"\\\\\"then local escaped=state.source:sub(state.index,state.index);state.index=state.index+1 local values={n=\"\\n\",r=\"\\r\",t=\"\\t\",[\"\\\\\"]=\"\\\\\",[\"\\\"\"]=\"\\\"\",[\"'\"]=\"'\"}if not values[escaped]then return fail(\"unsupported string escape\")end out[#out+1]=values[escaped]elseif char:byte()<32 then return fail(\"control character in string\")else out[#out+1]=char end end return fail(\"unterminated string\")end local function parseTable()state.depth=state.depth+1 if state.depth>32 then return fail(\"recipe data is nested too deeply\")end state.index=state.index+1 local out,nextIndex,keys={},1,{}skip()while state.index<=state.length and state.source:sub(state.index,state.index)~=\"}\"do state.nodes=state.nodes+1 if state.nodes>4096 then return fail(\"recipe data has too many values\")end local key,value,err local start=state.index if state.source:sub(state.index,state.index)==\"[\"then state.index=state.index+1;key,err=parseValue();if err then return nil,err end skip();if state.source:sub(state.index,state.index)~=\"]\"then return fail(\"missing closing recipe key bracket\")end state.index=state.index+1;skip();if state.source:sub(state.index,state.index)~=\"=\"then return fail(\"missing recipe key assignment\")end state.index=state.index+1;skip();value,err=parseValue();if err then return nil,err end else local named=identifier();skip()if named and state.source:sub(state.index,state.index)==\"=\"then key,state.index=named,state.index+1;skip();value,err=parseValue();if err then return nil,err end else state.index=start;value,err=parseValue();if err then return nil,err end key,nextIndex=nextIndex,nextIndex+1 end end if type(key)~=\"string\"and type(key)~=\"number\"then return fail(\"recipe table key must be text or a number\")end local signature=type(key)..\":\"..tostring(key)if keys[signature]then return fail(\"duplicate recipe table key\")end keys[signature],out[key]=true,value skip();local separator=state.source:sub(state.index,state.index)if separator==\",\"or separator==\";\"then state.index=state.index+1;skip()elseif separator~=\"}\"then return fail(\"expected recipe table separator\")end end if state.source:sub(state.index,state.index)~=\"}\"then return fail(\"unterminated recipe table\")end state.index,state.depth=state.index+1,state.depth-1 return out end parseValue=function()skip();local char=state.source:sub(state.index,state.index)if char==\"{\"then return parseTable()end if char==\"\\\"\"or char==\"'\"then return parseString()end local number=state.source:match(\"^-?%d+\",state.index)if number then state.index=state.index+#number;return tonumber(number)end local name=identifier()if name==\"true\"then return true elseif name==\"false\"then return false elseif name==\"nil\"then return nil elseif name then return fail(\"bare recipe values are not allowed\")end return fail(\"invalid recipe value\")end skip()if identifier()~=\"return\"then return fail(\"recipe data must start with return\")end local value,err=parseValue();if err then return nil,err end skip();if state.index<=state.length then return fail(\"unexpected content after recipe table\")end return value end local function validTags(tags)if tags==nil then return{}end if type(tags)~=\"table\"then return nil,\"recipe tags must be a table\"end local out={}for tag,members in pairs(tags)do local key=tostring(tag):gsub(\"^#\",\"\")if not key:match(\"^[%w_.:%-]+$\")or type(members)~=\"table\"or#members<1 then return nil,\"invalid recipe tag \"..tostring(tag)end out[key]={}for _,member in ipairs(members)do if not validReference(member)or tostring(member):sub(1,1)==\"#\"then return nil,\"invalid member of #\"..key end out[key][#out[key]+1]=member end table.sort(out[key])end return out end function M.validateRecipeGraph(recipes,tags)local tagMap,tagErr=validTags(tags);if not tagMap then return nil,tagErr end local visiting,complete={},{}local function visit(name)if complete[name]then return true end if visiting[name]then return nil,\"recipe cycle at \"..name end local recipe=recipes[name]if type(recipe)==\"table\"and type(recipe.alternatives)==\"table\"then recipe=recipe.alternatives[1]end if not recipe then return true end visiting[name]=true for _,input in ipairs(recipeIngredients(recipe,1))do local ingredient=input.name local members=ingredient:sub(1,1)==\"#\"and tagMap[ingredient:sub(2)]or{ingredient}for _,member in ipairs(members or{})do if recipes[member]then local ok,err=visit(member);if not ok then return nil,err end end end end visiting[name],complete[name]=nil,true return true end for name in pairs(recipes)do local ok,err=visit(name);if not ok then return nil,err end end return true end function M.parseRecipeOverlay(source)local value,err=dataParser(source,M.RECIPE_PACK_MAX_BYTES)if not value then return nil,err end if type(value)~=\"table\"then return nil,\"recipe overlay must return a table\"end local recipes,tags if value.schema==1 and type(value.recipes)==\"table\"then recipes,tags=value.recipes,value.tags or{}else recipes,tags=value,{}end local valid,validationErr=M.validateRecipes(recipes)if not valid then return nil,validationErr end local tagMap,tagsErr=validTags(tags);if not tagMap then return nil,tagsErr end local graphOk,graphErr=M.validateRecipeGraph(recipes,tagMap)if not graphOk then return nil,graphErr end return{schema=1,recipes=recipes,tags=tagMap}end function M.parseRecipeFile(source)local overlay,err=M.parseRecipeOverlay(source)return overlay and overlay.recipes or nil,err end function M.parseRecipePack(source)local pack,err=dataParser(source,M.RECIPE_PACK_MAX_BYTES)if not pack then return nil,err end if type(pack)~=\"table\"or pack.schema~=1 or type(pack.recipes)~=\"table\"then return nil,\"recipe pack must return { schema = 1, recipes = { ... } }\"end local valid,validationErr=M.validateRecipes(pack.recipes)if not valid then return nil,validationErr end local tagMap,tagsErr=validTags(pack.tags or{});if not tagMap then return nil,tagsErr end local graphOk,graphErr=M.validateRecipeGraph(pack.recipes,tagMap)if not graphOk then return nil,graphErr end return{schema=1,recipes=pack.recipes,tags=tagMap}end function M.recipeEqual(left,right)if type(left)~=\"table\"or type(right)~=\"table\"then return false end if type(left.alternatives)==\"table\"then left=left.alternatives[1]end if type(right.alternatives)==\"table\"then right=right.alternatives[1]end if not left or not right or left.mode~=right.mode or left.kind~=right.kind or left.output~=right.output or left.address~=right.address then return false end for slot=1,9 do if left.grid and right.grid and left.grid[slot]~=right.grid[slot]then return false end end local a,b=recipeIngredients(left,1),recipeIngredients(right,1)if#a~=#b then return false end for index=1,#a do if a[index].name~=b[index].name or a[index].count~=b[index].count then return false end end return true end function M.reviewRecipePack(existing,incoming)local validExisting,existingErr=M.validateRecipes(existing)if not validExisting then return nil,existingErr end local validIncoming,incomingErr=M.validateRecipes(incoming)if not validIncoming then return nil,incomingErr end local rows,counts={},{new=0,unchanged=0,conflict=0,invalid=0}for name,recipe in pairs(incoming)do local prior=existing[name]local status=not prior and\"new\"or(M.recipeEqual(prior,recipe)and\"unchanged\"or\"conflict\")counts[status]=counts[status]+1 rows[#rows+1]={name=name,status=status,incoming=recipe,existing=prior,selected=status==\"new\"}end table.sort(rows,function(a,b)return a.name<b.name end)return{rows=rows,counts=counts}end local function cloneRecipe(recipe)if type(recipe)==\"table\"and type(recipe.alternatives)==\"table\"then recipe=recipe.alternatives[1]end local copy={mode=recipe.mode,kind=recipe.kind,output=recipe.output,address=recipe.address,grid={},ingredients={}}for slot=1,9 do copy.grid[slot]=recipe.grid and recipe.grid[slot]end for _,input in ipairs(recipe.ingredients or{})do copy.ingredients[#copy.ingredients+1]={id=input.id,count=input.count,tag=input.tag}end return copy end function M.mergeRecipePack(existing,review,existingTags,incomingTags)if type(existing)~=\"table\"or type(review)~=\"table\"or type(review.rows)~=\"table\"then return nil,\"invalid recipe import review\"end local merged,imported={},0 for name,recipe in pairs(existing)do merged[name]=cloneRecipe(recipe)end for _,row in ipairs(review.rows)do if row.status==\"new\"and row.selected==true then merged[row.name],imported=cloneRecipe(row.incoming),imported+1 elseif row.status==\"conflict\"and row.selected==true then merged[row.name],imported=cloneRecipe(row.incoming),imported+1 end end local tags={}for name,members in pairs(existingTags or{})do tags[name]=members end for name,members in pairs(incomingTags or{})do tags[name]=members end local valid,err=M.validateRecipes(merged);if not valid then return nil,err end local graphOk,graphErr=M.validateRecipeGraph(merged,tags);if not graphOk then return nil,graphErr end return merged,imported,tags end function M.encodeRecipeFile(recipes,tags)local valid,err=M.validateRecipes(recipes);if not valid then return nil,err end local tagMap,tagsErr=validTags(tags or{});if not tagMap then return nil,tagsErr end local graphOk,graphErr=M.validateRecipeGraph(recipes,tagMap);if not graphOk then return nil,graphErr end local ok,encoded=pcall(textutils.serialise,{schema=1,recipes=recipes,tags=tagMap},{compact=true})if not ok or type(encoded)~=\"string\"then return nil,\"could not serialise recipes\"end return\"return \"..encoded..\"\\n\"end function M.writeRecipeFile(path,recipes,tags)if type(path)~=\"string\"or path==\"\"then return false,\"invalid recipe file path\"end local raw,encodeErr=M.encodeRecipeFile(recipes,tags);if not raw then return false,encodeErr end local parent=fs.getDir(path);if parent~=\"\"and not fs.exists(parent)then fs.makeDir(parent)end local temporary,backup=path..\".new\",path..\".bak\"if fs.exists(temporary)then fs.delete(temporary)end local handle=fs.open(temporary,\"w\");if not handle then return false,\"could not stage recipes\"end handle.write(raw);handle.close()local check=fs.open(temporary,\"r\");local staged=check and check.readAll()or nil;if check then check.close()end if not M.parseRecipeOverlay(staged or\"\")then if fs.exists(temporary)then fs.delete(temporary)end;return false,\"could not validate staged recipes\"end if fs.exists(backup)then fs.delete(backup)end if fs.exists(path)then fs.move(path,backup)end local moved,moveErr=pcall(fs.move,temporary,path)if not moved then if fs.exists(backup)and not fs.exists(path)then fs.move(backup,path)end;if fs.exists(temporary)then fs.delete(temporary)end;return false,tostring(moveErr)end if fs.exists(backup)then fs.delete(backup)end return true end local function selectedRecipe(entry)if type(entry)==\"table\"and type(entry.alternatives)==\"table\"then return entry.alternatives[1]end return entry end local function tagMembers(tags,reference)if reference:sub(1,1)~=\"#\"then return{reference}end local members=tags[reference:sub(2)]if type(members)~=\"table\"or#members==0 then return nil,\"no known members for \"..reference end return members end function M.normaliseTagChoices(input)if input==nil then return{}end if type(input)~=\"table\"then return nil,\"invalid tag choices\"end local out,count={},0 for tag,member in pairs(input)do count=count+1 if count>32 or not validReference(tag)or tostring(tag):sub(1,1)~=\"#\"or not validReference(member)or tostring(member):sub(1,1)==\"#\"then return nil,\"invalid tag choice\"end out[tag]=member end return out end local function recipeTags(recipe)local seen,out={},{}local function add(value)if type(value)==\"string\"and value:sub(1,1)==\"#\"and validReference(value)and not seen[value]then seen[value],out[#out+1]=true,value end end recipe=selectedRecipe(recipe)for _,ingredient in ipairs(type(recipe)==\"table\"and recipeIngredients(recipe,1)or{})do add(ingredient.name)end for slot=1,9 do add(type(recipe)==\"table\"and recipe.grid and recipe.grid[slot])end table.sort(out)return out end function M.recipeTagReferences(recipe)return recipeTags(recipe)end function M.recipeTagOptions(recipes,tags,item,stock)if type(item)~=\"string\"or not validReference(item)or item:sub(1,1)==\"#\"then return nil,\"invalid recipe item\"end local recipe=selectedRecipe(type(recipes)==\"table\"and recipes[item])if type(recipe)~=\"table\"then return nil,\"recipe is unavailable\"end local available,rows=M.normalise(stock),{}for _,tag in ipairs(recipeTags(recipe))do local members,err=tagMembers(type(tags)==\"table\"and tags or{},tag)if not members then return nil,err end local options={}for _,name in ipairs(members)do if validReference(name)and tostring(name):sub(1,1)~=\"#\"then options[#options+1]={name=name,count=available[name]or 0,craftable=type(recipes)==\"table\"and recipes[name]~=nil}end end table.sort(options,function(a,b)if a.count~=b.count then return a.count>b.count end if a.craftable~=b.craftable then return a.craftable end return a.name<b.name end)if#options==0 then return nil,\"no known members for \"..tag end rows[#rows+1]={tag=tag,options=options,default=options[1].name}end return{item=item,tags=rows}end local function availabilityScore(name,tags,available,recipes,visiting,memo)if type(name)~=\"string\"or not validReference(name)or name:sub(1,1)==\"#\"then return-math.huge end local stocked=tonumber(available[name])or 0 if stocked>0 then return 1000000000+math.min(stocked,1000000)end memo=memo or{}if memo[name]~=nil then return memo[name]end visiting=visiting or{}if visiting[name]then return-math.huge end local recipe=selectedRecipe(recipes[name])if type(recipe)~=\"table\"or recipe.special or not recipe.output or not recipe.address then memo[name]=-math.huge;return memo[name]end visiting[name]=true local score for _,ingredient in ipairs(recipeIngredients(recipe,1))do local candidate=-math.huge if ingredient.name:sub(1,1)==\"#\"then local members=tagMembers(tags,ingredient.name)if members then for _,member in ipairs(members)do candidate=math.max(candidate,availabilityScore(member,tags,available,recipes,visiting,memo))end end else candidate=availabilityScore(ingredient.name,tags,available,recipes,visiting,memo)end if candidate==-math.huge then score=-math.huge;break end score=score and math.min(score,candidate)or candidate end visiting[name]=nil if score==nil then score=-math.huge end if score>-math.huge then score=score-1 end memo[name]=score return score end local function chooseMember(reference,tags,available,recipes,choices,scores)local members,err=tagMembers(tags,reference);if not members then return nil,err end local chosen=choices and choices[reference]if chosen then local present=false for _,member in ipairs(members)do if member==chosen then present=true;break end end if not present then return nil,\"selected item is not a member of \"..reference end if(available[chosen]or 0)>0 or availabilityScore(chosen,tags,available,recipes,{},scores or{})>-math.huge then return chosen end return nil,\"selected item is neither stocked nor craftable: \"..chosen end local best,bestCount=nil,-1 for _,member in ipairs(members)do local count=available[member]or 0 if count>bestCount or(count==bestCount and(not best or member<best))then best,bestCount=member,count end end if best and bestCount>0 then return best end best,bestCount=nil,-math.huge for _,member in ipairs(members)do local score=availabilityScore(member,tags,available,recipes,{},scores or{})if score>bestCount or(score==bestCount and score>-math.huge and(not best or member<best))then best,bestCount=member,score end end if best then return best end return nil,\"no stocked or craftable member for \"..reference end local function requesterGrid(recipe,resolved,tags,available,recipes,choices,scores)local grid={}for slot=1,9 do local value=recipe.grid and recipe.grid[slot]if type(value)==\"string\"and value:sub(1,1)==\"#\"then local concrete=resolved[value]if not concrete then concrete=select(1,chooseMember(value,tags,available,recipes,choices,scores))end if not concrete then return nil,\"no concrete item available for requester tag \"..value end grid[slot]=concrete else grid[slot]=value end end return grid end function M.plan(recipes,stock,item,quantity,options)quantity=tonumber(quantity)if type(item)~=\"string\"or item==\"\"or not quantity or quantity<1 or quantity~=math.floor(quantity)then return nil,\"invalid item or quantity\"end local valid,errorText=M.validateRecipes(recipes)if not valid then return nil,errorText end options=type(options)==\"table\"and options or{}local choices,choiceErr=M.normaliseTagChoices(options.tagChoices)if not choices then return nil,choiceErr end local available,plan,visiting,tags,choiceScores=M.normalise(stock),{},{},type(options.tags)==\"table\"and options.tags or{},{}local function requireItem(name,needed)local used=options.forceRoot==true and name==item and 0 or math.min(available[name]or 0,needed)available[name]=(available[name]or 0)-used local missing=needed-used if missing<=0 then return true end if visiting[name]then return nil,\"recipe cycle at \"..name end local recipe=selectedRecipe(recipes[name])if not recipe then return nil,\"missing recipe for \"..name..\" (need \"..tostring(missing)..\")\"end if recipe.special or not recipe.output or not recipe.address then return nil,\"recipe \"..name..\" is not supported by Crafting Queue\"end visiting[name]=true local batches=math.ceil(missing/recipe.output)local inputs,resolved={},{}for _,ingredient in ipairs(recipeIngredients(recipe,batches))do local concrete,resolveErr=chooseMember(ingredient.name,tags,available,recipes,choices,choiceScores)if not concrete then return nil,resolveErr end inputs[#inputs+1]={name=concrete,count=ingredient.count,source=ingredient.name,tag=ingredient.name:sub(1,1)==\"#\"and ingredient.name or nil}if ingredient.name:sub(1,1)==\"#\"then resolved[ingredient.name]=concrete end local ok,err=requireItem(concrete,ingredient.count)if not ok then return nil,err end end visiting[name]=nil local produced=batches*recipe.output available[name]=(available[name]or 0)+produced-missing local dispatch=recipe.address==\"Crafter\"and\"requester\"or\"package\"if dispatch==\"requester\"and type(recipe.grid)~=\"table\"then return nil,\"Crafter recipe \"..name..\" has no 3x3 grid\"end local grid,gridErr=recipe.grid,nil if dispatch==\"requester\"then grid,gridErr=requesterGrid(recipe,resolved,tags,available,recipes,choices,choiceScores)if not grid then return nil,gridErr end end plan[#plan+1]={name=name,output=recipe.output,batches=batches,produced=produced,requested=missing,address=recipe.address,kind=recipe.kind,dispatch=dispatch,grid=grid,sourceGrid=recipe.grid,inputs=inputs,}return true end local ok,err=requireItem(item,quantity)return ok and plan or nil,err end function M.validateExecutionPlan(plan)if type(plan)~=\"table\"or#plan<1 or#plan>96 then return nil,\"invalid remote recipe plan\"end for _,step in ipairs(plan)do if type(step)~=\"table\"or not validReference(step.name)or tostring(step.name):sub(1,1)==\"#\"then return nil,\"invalid remote recipe output\"end if type(step.address)~=\"string\"or step.address==\"\"or#step.address>64 or step.address:find(\"[%c]\")then return nil,\"invalid remote recipe address\"end if step.dispatch~=\"requester\"and step.dispatch~=\"package\"then return nil,\"invalid remote recipe route\"end if(step.address==\"Crafter\")~=(step.dispatch==\"requester\")then return nil,\"invalid remote recipe executor\"end for _,field in ipairs({\"output\",\"batches\",\"produced\",\"requested\"})do local value=tonumber(step[field])if not value or value<1 or value~=math.floor(value)or value>65536 then return nil,\"invalid remote recipe quantity\"end end if step.produced~=step.batches*step.output then return nil,\"remote recipe output does not match its batch count\"end if step.dispatch==\"requester\"then if type(step.grid)~=\"table\"then return nil,\"remote Crafter recipe has no grid\"end for slot=1,9 do local value=step.grid[slot]if value~=nil and value~=false and not validReference(value)then return nil,\"invalid remote recipe grid\"end if type(value)==\"string\"and value:sub(1,1)==\"#\"then return nil,\"remote requester grid contains unresolved tag\"end end end if type(step.inputs)~=\"table\"or#step.inputs>64 then return nil,\"invalid remote recipe inputs\"end for _,input in ipairs(step.inputs)do if type(input)~=\"table\"or not validReference(input.name)or tostring(input.name):sub(1,1)==\"#\"then return nil,\"invalid remote recipe input\"end local count=tonumber(input.count)if not count or count<1 or count~=math.floor(count)or count>65536 then return nil,\"invalid remote recipe input quantity\"end if input.source~=nil and not validReference(input.source)then return nil,\"invalid remote recipe input source\"end end end return plan end return M",
  ["ceetos/lib/crafting_service.lua"] = "local M={COMMAND_EVENT=\"ceetos_crafting_command\",HEARTBEAT_EVENT=\"ceetos_crafting_wake\",}local Service={}Service.__index=Service local function clampDelay(value)value=tonumber(value)or 0.25 return math.max(0.05,math.min(value,1))end function M.new(options)options=options or{}assert(type(options.tick)==\"function\",\"crafting service requires tick\")assert(type(options.startTimer)==\"function\",\"crafting service requires startTimer\")return setmetatable({tick=options.tick,startTimer=options.startTimer,pullEvent=options.pullEvent,now=type(options.now)==\"function\"and options.now or nil,started=false,timer=nil,dueAt=0,runs=0,},Service)end function Service:runTick()local delay=clampDelay(self.tick())self.timer=self.startTimer(delay)self.dueAt=self.now and(self.now()+math.floor(delay*1000))or 0 self.runs=self.runs+1 return delay end function Service:start()if self.started then return false end self.started=true self:runTick()return true end function Service:shouldWake(event)if type(event)~=\"table\"then return false end if event[1]==M.COMMAND_EVENT or event[1]==M.HEARTBEAT_EVENT then return true end if event[1]==\"timer\"and event[2]==self.timer then return true end return self.now~=nil and self.now()>=self.dueAt end function Service:step(event)if not self.started then return self:start()end if not self:shouldWake(event)then return false end self:runTick()return true end function Service:run()assert(type(self.pullEvent)==\"function\",\"crafting service requires pullEvent\")self:start()while true do self:step({self.pullEvent()})end end return M",
  ["ceetos/lib/craft_sources.lua"] = "local M={}local function isType(value,needle)return tostring(value or\"\"):lower():find(needle,1,true)~=nil end function M.kind(value)if type(value)==\"string\"then return{transport=\"local\",name=value}end return type(value)==\"table\"and value or nil end function M.key(value)value=M.kind(value)if not value then return nil end return tostring(value.transport or\"local\")..\":\"..tostring(value.peer or\"\")..\":\"..tostring(value.name or\"\")end function M.label(value)value=M.kind(value)if not value then return\"Not selected\"end if value.transport==\"peer\"then return\"Peer \"..tostring(value.peer)..\" / \"..tostring(value.name)end return tostring(value.name)end function M.discoverLocal()local tickers,requesters={},{}for _,name in ipairs(peripheral.getNames())do local typeName=peripheral.getType(name)if isType(typeName,\"stockticker\")then tickers[#tickers+1]={transport=\"local\",name=name,type=typeName,label=\"Local / \"..name}elseif isType(typeName,\"redstonerequester\")then requesters[#requesters+1]={transport=\"local\",name=name,type=typeName,label=\"Local / \"..name}end end table.sort(tickers,function(a,b)return a.name<b.name end)table.sort(requesters,function(a,b)return a.name<b.name end)return tickers,requesters end function M.matches(value,kind)value=M.kind(value)if not value or type(value.name)~=\"string\"then return false end local needle=(kind==\"ticker\"or kind==\"stockticker\")and\"stockticker\"or\"redstonerequester\"return isType(value.type,needle)end return M",
  ["ceetos/lib/program_capabilities.lua"] = "local store=require(\"ceetos.lib.store\")local auth=require(\"ceetos.lib.auth\")local audit=require(\"ceetos.lib.audit\")local M={}local PATH=\"/ceetos/data/program-capabilities.lua\"local SCHEMA=1 local SCRATCH_ROOT=\"/ceetos/data/programs\"local PROTECTED_ROOTS={\"/ceetos\",\"/ceetos-dev\",\"/rom\",}local READ_ONLY_METHODS={getAngles=true,getSpeed=true,getStress=true,getStressCapacity=true,getTargetSpeed=true,getItemDetail=true,getInventoryName=true,getMetadata=true,getFluidDetail=true,getEnergy=true,getCursorPos=true,getSize=true,getTextScale=true,isRunning=true,isPresent=true,list=true,size=true,}local function now()return os.epoch(\"utc\")end local function copy(value,seen)if type(value)~=\"table\"then return value end seen=seen or{}if seen[value]then return seen[value]end local result={}seen[value]=result for key,item in pairs(value)do result[copy(key,seen)]=copy(item,seen)end return result end local function defaultConfig()return{schema=SCHEMA,programs={}}end local function normalise(path)if type(path)~=\"string\"or path==\"\"or#path>240 or path:find(\"[%z\\1-\\31]\")then return nil,\"invalid program path\"end local parts={}for part in path:gmatch(\"[^/]+\")do if part==\"..\"then if#parts>0 then table.remove(parts)end elseif part~=\".\"and part~=\"\"then parts[#parts+1]=part end end return\"/\"..table.concat(parts,\"/\")end local function readSource(path)local handle,err=fs.open(path,\"r\")if not handle then return nil,err or\"could not read program\"end local source=handle.readAll()handle.close()return source end local function legacyHash(source)local hash=2166136261 for index=1,#source do hash=bit32.bxor(hash,source:byte(index))hash=(hash*16777619)%4294967296 end return string.format(\"fnv1a32:%08x\",hash)end local function programId(path)return\"p-\"..legacyHash(path):match(\":(%x+)$\")end local function loadConfig()local config=store.readFresh(PATH,defaultConfig())if type(config)~=\"table\"then return defaultConfig()end if type(config.programs)~=\"table\"then config.programs={}end config.schema=SCHEMA return config end local function saveConfig(config)local ok,err=store.writeAtomic(PATH,config)assert(ok,\"could not save program capabilities: \"..tostring(err))end local function adminActor()if auth.refreshSession then auth.refreshSession()end auth.require(\"admin\")return auth.current and auth.current.name or\"admin\"end local function pathWithin(path,root)return path==root or path:sub(1,#root+1)==root..\"/\"end local function protectedProgramPath(path)if path==\"/startup.lua\"then return true end for _,root in ipairs(PROTECTED_ROOTS)do if pathWithin(path,root)then return true end end return false end local function normaliseProgramPath(path)local normalized,err=normalise(path)if not normalized then return nil,err end if protectedProgramPath(normalized)then return nil,\"protected CeetOS/CraftOS program path\"end if normalized:sub(-4):lower()~=\".lua\"then return nil,\"program must be a Lua file\"end if not fs.exists(normalized)or fs.isDir(normalized)then return nil,\"program file not found\"end return normalized end local function defaultRecord(path,source,owner)return{path=path,id=programId(path),sourceHash=legacyHash(source),hashKind=\"fnv1a32-legacy-integrity-marker\",owner=owner or\"unassigned\",trust=\"sandboxed\",capabilities={scratch=true,peripherals={}},createdAt=now(),updatedAt=now(),}end local function cleanMethods(methods)if type(methods)~=\"table\"then return nil,\"methods must be a list\"end local seen,result={},{}for _,method in ipairs(methods)do if type(method)~=\"string\"or not READ_ONLY_METHODS[method]then return nil,\"method is not an approved read-only capability\"end if not seen[method]then seen[method]=true;result[#result+1]=method end end if#result==0 then return nil,\"at least one read-only method is required\"end table.sort(result)return result end local function recordIdAvailable(config,id,path)for savedPath,saved in pairs(config.programs)do if savedPath~=path and type(saved)==\"table\"and saved.id==id then return false end end return true end local function recordFrom(config,path,source)local saved=config.programs[path]local currentHash=legacyHash(source)local id=programId(path)if not recordIdAvailable(config,id,path)then return nil,nil,\"program capability ID collision; choose a different program path\"end if type(saved)==\"table\"and saved.sourceHash==currentHash and saved.path==path then local record=copy(saved)record.id=id record.hashKind=\"fnv1a32-legacy-integrity-marker\"record.capabilities=type(record.capabilities)==\"table\"and record.capabilities or{}record.capabilities.scratch=true record.capabilities.peripherals=type(record.capabilities.peripherals)==\"table\"and record.capabilities.peripherals or{}return record,false,nil end local record=defaultRecord(path,source,type(saved)==\"table\"and saved.owner or nil)record.stale=type(saved)==\"table\"return record,record.stale,nil end local function scratchPath(record)return SCRATCH_ROOT..\"/\"..record.id end local function normaliseScratch(path,root)if type(path)~=\"string\"or#path>240 or path:find(\"[%z\\1-\\31]\")then return nil end local candidate=path:sub(1,1)==\"/\"and path or root..\"/\"..path local normalized=normalise(candidate)if not normalized or not pathWithin(normalized,root)then return nil end return normalized end local function safeTerm()local source=term return{write=source.write,blit=source.blit,clear=source.clear,clearLine=source.clearLine,scroll=source.scroll,getSize=source.getSize,getCursorPos=source.getCursorPos,setCursorPos=source.setCursorPos,getCursorBlink=source.getCursorBlink,setCursorBlink=source.setCursorBlink,setTextColor=source.setTextColor,setTextColour=source.setTextColour,getTextColor=source.getTextColor,getTextColour=source.getTextColour,setBackgroundColor=source.setBackgroundColor,setBackgroundColour=source.setBackgroundColour,getBackgroundColor=source.getBackgroundColor,getBackgroundColour=source.getBackgroundColour,isColor=source.isColor,isColour=source.isColour,}end local function safeTextutils()return{serialise=textutils.serialise,serialize=textutils.serialize,serialiseJSON=textutils.serialiseJSON,serializeJSON=textutils.serializeJSON,unserialiseJSON=textutils.unserialiseJSON,unserializeJSON=textutils.unserializeJSON,formatTime=textutils.formatTime,urlEncode=textutils.urlEncode,}end local function tableCopy(value)local output={}for key,item in pairs(value or{})do output[key]=item end return output end local function safeFilesystem(record,actor,deny)local root=scratchPath(record)if not fs.exists(root)then local made,makeErr=pcall(fs.makeDir,root)if not made or makeErr==false then error(\"could not initialise program scratch storage\",2)end end local function target(path)local value=normaliseScratch(path,root)if not value then deny(\"filesystem path\")end return value end local function mutable(path)local value=target(path)if value==root then deny(\"program scratch root\")end return value end local proxy={}function proxy.exists(path)return fs.exists(target(path))end function proxy.isDir(path)return fs.isDir(target(path))end function proxy.list(path)return fs.list(target(path))end function proxy.getSize(path)return fs.getSize(target(path))end function proxy.open(path,mode)mode=tostring(mode or\"r\")if mode~=\"r\"and mode~=\"rb\"and mode~=\"w\"and mode~=\"wb\"and mode~=\"a\"and mode~=\"ab\"then deny(\"filesystem mode\")end return fs.open((mode==\"r\"or mode==\"rb\")and target(path)or mutable(path),mode)end function proxy.makeDir(path)return fs.makeDir(mutable(path))end function proxy.delete(path)return fs.delete(mutable(path))end function proxy.move(from,to)return fs.move(mutable(from),mutable(to))end function proxy.copy(from,to)return fs.copy(mutable(from),mutable(to))end function proxy.combine(a,b)local value=normaliseScratch(tostring(a or\"\")..\"/\"..tostring(b or\"\"),root)if not value then deny(\"filesystem path\")end return value end function proxy.getDir(path)return fs.getDir(target(path))end function proxy.getName(path)return fs.getName(target(path))end return proxy end local function safePeripheral(record,deny)local grants=record.trust==\"trusted\"and record.capabilities and record.capabilities.peripherals or{}local function methodsFor(name)local grant=type(grants)==\"table\"and grants[name]or nil local allowed={}for _,method in ipairs(type(grant)==\"table\"and grant.methods or{})do if READ_ONLY_METHODS[method]then allowed[method]=true end end return allowed end local proxy={}function proxy.getNames()local result={}for name in pairs(grants or{})do local methods=methodsFor(name)if next(methods)and peripheral.isPresent(name)then result[#result+1]=name end end table.sort(result)return result end function proxy.isPresent(name)return next(methodsFor(name))~=nil and peripheral.isPresent(name)or false end function proxy.getType(name)if next(methodsFor(name))==nil then deny(\"peripheral \"..tostring(name))end return peripheral.getType(name)end function proxy.getMethods(name)local result={}for method in pairs(methodsFor(name))do result[#result+1]=method end if#result==0 then deny(\"peripheral \"..tostring(name))end table.sort(result)return result end function proxy.wrap(name)local allowed=methodsFor(name)if next(allowed)==nil then deny(\"peripheral \"..tostring(name))end local wrapped={}for method in pairs(allowed)do wrapped[method]=function(...)local raw=peripheral.wrap(name)if not raw or type(raw[method])~=\"function\"then error(\"granted peripheral is unavailable\",2)end local args={...}if args[1]==wrapped then table.remove(args,1)end return raw[method](table.unpack(args))end end return wrapped end return proxy end function M.normaliseProgramPath(path)return normaliseProgramPath(path)end function M.legacySourceHash(source)assert(type(source)==\"string\",\"source must be a string\")return legacyHash(source)end function M.isReadOnlyMethod(method)return READ_ONLY_METHODS[method]==true end function M.describe(path)local normalized,err=normaliseProgramPath(path)if not normalized then return nil,err end local source,readErr=readSource(normalized)if not source then return nil,readErr end local record,_,recordErr=recordFrom(loadConfig(),normalized,source)if not record then return nil,recordErr end return copy(record)end function M.create(path,owner)local actor=adminActor()local normalized,err=normaliseProgramPath(path)assert(normalized,err)if owner~=nil then assert(type(owner)==\"string\"and owner:match(\"^[%w_.%-]+$\")and#owner<=64,\"invalid program owner\")end local source=assert(readSource(normalized))local config=loadConfig()assert(recordIdAvailable(config,programId(normalized),normalized),\"program capability ID collision; choose a different program path\")local record=defaultRecord(normalized,source,owner or actor)config.programs[normalized]=record saveConfig(config)audit.log(actor,\"program.capability_create\",{path=normalized,owner=record.owner})return copy(record)end function M.setTrust(path,trust)local actor=adminActor()assert(trust==\"sandboxed\"or trust==\"trusted\",\"invalid program trust state\")local normalized,err=normaliseProgramPath(path)assert(normalized,err)local source=assert(readSource(normalized))local config=loadConfig()local record,_,recordErr=recordFrom(config,normalized,source)assert(record,recordErr)record.trust,record.updatedAt=trust,now()if trust~=\"trusted\"then record.capabilities.peripherals={}end config.programs[normalized]=record saveConfig(config)audit.log(actor,\"program.trust\",{path=normalized,trust=trust})return copy(record)end function M.setPortableTrust(path,mountToken,durationMs)local actor=adminActor()local normalized,err=normaliseProgramPath(path)assert(normalized,err)assert(type(mountToken)==\"string\"and(mountToken:match(\"^disk%-%w[%w%-%_]*:%d+$\")or mountToken:match(\"^drive%-%w[%w%-%_]*:%d+$\")),\"invalid removable-media mount token\")durationMs=math.max(60000,math.min(8*60*60*1000,math.floor(tonumber(durationMs)or 15*60*1000)))local source=assert(readSource(normalized))local config=loadConfig()local record,_,recordErr=recordFrom(config,normalized,source)assert(record,recordErr)record.portable={token=mountToken,expiresAt=now()+durationMs,grantedBy=actor}record.updatedAt=now()config.programs[normalized]=record saveConfig(config)audit.log(actor,\"program.portable_trust\",{path=normalized,durationMs=durationMs})return copy(record)end function M.hasPortableTrust(path,mountToken)if type(mountToken)~=\"string\"then return false end local record,err=M.describe(path)if not record then return false,err end local portable=type(record.portable)==\"table\"and record.portable or nil return portable~=nil and portable.token==mountToken and tonumber(portable.expiresAt or 0)>now(),record end function M.setOwner(path,owner)local actor=adminActor()assert(type(owner)==\"string\"and owner:match(\"^[%w_.%-]+$\")and#owner<=64,\"invalid program owner\")local normalized,err=normaliseProgramPath(path)assert(normalized,err)local source=assert(readSource(normalized))local config=loadConfig()local record,_,recordErr=recordFrom(config,normalized,source)assert(record,recordErr)record.owner,record.updatedAt=owner,now()config.programs[normalized]=record saveConfig(config)audit.log(actor,\"program.owner\",{path=normalized,owner=owner})return copy(record)end function M.grantReadOnlyPeripheral(path,name,methods)local actor=adminActor()assert(type(name)==\"string\"and name~=\"\"and#name<=128 and not name:find(\"[%z\\1-\\31]\"),\"invalid peripheral name\")local clean,methodErr=cleanMethods(methods)assert(clean,methodErr)local normalized,err=normaliseProgramPath(path)assert(normalized,err)local source=assert(readSource(normalized))local config=loadConfig()local record,_,recordErr=recordFrom(config,normalized,source)assert(record,recordErr)assert(record.trust==\"trusted\",\"admin must trust the program before granting peripherals\")record.capabilities.peripherals[name]={methods=clean}record.updatedAt=now()config.programs[normalized]=record saveConfig(config)audit.log(actor,\"program.peripheral_grant\",{path=normalized,peripheral=name,methods=clean})return copy(record)end function M.revokePeripheral(path,name)local actor=adminActor()local normalized,err=normaliseProgramPath(path)assert(normalized,err)local source=assert(readSource(normalized))local config=loadConfig()local record,_,recordErr=recordFrom(config,normalized,source)assert(record,recordErr)record.capabilities.peripherals[name]=nil record.updatedAt=now()config.programs[normalized]=record saveConfig(config)audit.log(actor,\"program.peripheral_revoke\",{path=normalized,peripheral=tostring(name)})return copy(record)end function M.viewerEnvironment(path,actor)local record,err=M.describe(path)if not record then return nil,err end actor=actor or\"viewer\"local denied={}local function deny(action)if not denied[action]then denied[action]=true audit.log(actor,\"program.denied\",{path=record.path,action=action})end error(\"permission denied: \"..action,3)end local function pullEvent(filter)while true do local event={os.pullEventRaw(filter)}if event[1]~=\"terminate\"then return table.unpack(event)end end end local safeOs={pullEvent=pullEvent,startTimer=os.startTimer,cancelTimer=os.cancelTimer,clock=os.clock,time=os.time,day=os.day,epoch=os.epoch,getComputerID=os.getComputerID,getComputerLabel=os.getComputerLabel,}local environment={_VERSION=_VERSION,assert=assert,error=error,ipairs=ipairs,next=next,pairs=pairs,pcall=pcall,select=select,tonumber=tonumber,tostring=tostring,type=type,xpcall=xpcall,print=print,printError=printError,read=read,write=write,math=tableCopy(math),string=tableCopy(string),table=tableCopy(table),coroutine=tableCopy(coroutine),bit32=tableCopy(bit32),utf8=tableCopy(utf8),keys=tableCopy(keys),colors=tableCopy(colors),colours=tableCopy(colours or colors),term=safeTerm(),textutils=safeTextutils(),os=safeOs,fs=safeFilesystem(record,actor,deny),peripheral=safePeripheral(record,deny),}return setmetatable(environment,{__index=function()return nil end}),record end function M.loadViewer(path,actor)local environment,record=M.viewerEnvironment(path,actor)if not environment then return nil,record end local chunk,err=loadfile(record.path,nil,environment)if not chunk then audit.log(actor or\"viewer\",\"program.denied\",{path=record.path,action=\"load\"})return nil,err end return chunk,record end function M.scratchPath(path)local record,err=M.describe(path)if not record then return nil,err end return scratchPath(record)end M.PATH=PATH M.SCHEMA=SCHEMA M.HASH_KIND=\"fnv1a32-legacy-integrity-marker\"return M",
  ["ceetos/bin/ceetver.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local ok,version=pcall(require,\"ceetos.lib.version\")print(\"CeetOS \"..(ok and version.string or\"0.12.0\"))",
  ["ceetos/bin/ceetdevices.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local peripherals=require(\"ceetos.lib.peripherals\")local shared={}for _,item in ipairs(peripherals.describe())do shared[item.name]=item.mode end for _,item in ipairs(peripherals.listLocal())do print(item.name..\"\\t\"..tostring(item.type or\"unknown\")..\"\\t\"..(shared[item.name]or\"not shared\"))end",
  ["ceetos/bin/ceetcraft.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local auth,store,crafting=require(\"ceetos.lib.auth\"),require(\"ceetos.lib.store\"),require(\"ceetos.lib.crafting\")local telemetryOk,telemetry=pcall(require,\"ceetos.lib.telemetry\")if telemetryOk and telemetry and telemetry.markActivity then telemetry.markActivity(\"crafting queue\")end local terminalUi=require(\"ceetos.lib.terminal_ui\")local input=require(\"ceetos.lib.input\")local function canMutate()return auth.allowed(\"operator\")end local STATE,COMMANDS,COMMAND_RESULTS=\"/ceetos/data/crafting-state.lua\",\"/ceetos/data/crafting-commands\",\"/ceetos/data/crafting-command-results\"local COMMAND_EVENT=\"ceetos_crafting_command\"local query,filter,focus,selectedStock,selectedCart=\"\",\"all\",\"stock\",1,1 local RECIPE_FILE,RECIPE_IMPORTS=\"/ceetos/data/recipes.lua\",\"/ceetos/data/recipe-imports.lua\"local modal,pending,serial,armedAction,lastRowClick,feedback=nil,{},0,nil,nil,nil local importReview,importScroll,importArmed=nil,0,nil local lastState,stockScroll,cartScroll,actionMenu=nil,0,0,nil local stateDirty=true local function navigationKey(code)return input.navigation(keys.getName(code))end local actions={{label=\"Add\",action=\"add\"},{label=\"Tags T\",action=\"tags\"},{label=\"Edit\",action=\"edit\"},{label=\"Del\",action=\"delete\"},{label=\"Clear list\",action=\"clear_cart\"},{label=\"Craft\",action=\"mode\"},{label=\"Dest G\",action=\"destination\"},{label=\"Sources\",action=\"sources\"},{label=\"Search\",action=\"search\"},{label=\"Clear\",action=\"clear_search\"},{label=\"View\",action=\"filter\"},{label=\"R\",action=\"refresh\"},{label=\"Next recipes\",action=\"next_page\"},{label=\"Previous recipes\",action=\"previous_page\"},{label=\"Send O\",action=\"submit\"},{label=\"Retry\",action=\"retry\"},{label=\"Can C\",action=\"cancel\"},{label=\"Back\",action=\"back\"},}local function state(force)local fallback={stock={},cart={},queue={},retries={},acknowledgements={},config={orderAddress=\"Order\"},history={},recipeCatalog={rows={},total=0},tagChoices={}}if lastState and not force and not stateDirty then return lastState end if store.readStable then local value=store.readStable(STATE,nil)if type(value)==\"table\"then lastState,stateDirty=value,false;return value end return lastState or fallback end local value=store.read(STATE,fallback)if type(value)==\"table\"then lastState,stateDirty=value,false;return value end return lastState or fallback end local function craftingStateChanged(event,first,second)if event~=\"ceetos_state_changed\"then return false end return first==\"crafting\"or first==\"crafting-state\"or first==STATE or second==\"crafting\"or second==STATE end local function clipped(value,width)return terminalUi.clip(value,math.max(1,tonumber(width)or 1),\"~\")end local function queueCommand(action,data,label)feedback=nil serial=serial+1 local createdAt=os.epoch(\"utc\")local token=\"craft-ui-\"..tostring(createdAt)..\"-\"..tostring(serial)if not fs.exists(COMMANDS)then fs.makeDir(COMMANDS)end data=data or{};data.action,data.token,data.createdAt=action,token,createdAt local target,temporary=fs.combine(COMMANDS,token..\".lua\"),fs.combine(COMMANDS,token..\".tmp\")local handle=fs.open(temporary,\"w\")if not handle then return false,\"could not queue \"..tostring(label or action)end handle.write(textutils.serialise(data));handle.close()if fs.exists(target)then fs.delete(target)end fs.move(temporary,target)pending[token]={label=label or action,at=createdAt,path=target,receipt=fs.combine(COMMAND_RESULTS,token..\".lua\")}if os.queueEvent then pcall(os.queueEvent,COMMAND_EVENT,token)end return true end local function commandReceipt(path)if type(path)~=\"string\"then return nil end if store.readStable then return store.readStable(path,nil)end if store.invalidate then store.invalidate(path)end return store.read(path,nil)end local function cleanPending(s)for token,row in pairs(pending)do local acknowledgement=commandReceipt(row.receipt)or(s.acknowledgements or{})[token]if acknowledgement then pending[token]=nil if acknowledgement.ok~=true then feedback=\"Cannot \"..row.label..\": \"..tostring(acknowledgement.error or\"request was rejected\")end elseif os.epoch(\"utc\")-row.at>5000 then row.awaitingService=true end end end local function pendingText()for _,row in pairs(pending)do if row.awaitingService then return\"Queued: \"..row.label..\" (awaiting service)\"end return\"Pending: \"..row.label end return feedback end local function startModal(kind,label,value,replaceOnInput)modal={kind=kind,label=label,value=tostring(value or\"\"),replaceOnInput=replaceOnInput==true}end local function recordRecipeImport(entry)local history=store.read(RECIPE_IMPORTS,{})history[#history+1]=entry while#history>16 do table.remove(history,1)end store.write(RECIPE_IMPORTS,history)end local function currentOverlay(s)local handle=fs.exists(RECIPE_FILE)and fs.open(RECIPE_FILE,\"r\")or nil local source=handle and handle.readAll()or nil if handle then handle.close()end if source then local overlay,err=crafting.parseRecipeOverlay(source)if overlay then return overlay end return nil,err end return{schema=1,recipes={},tags={}}end local function beginRecipeImport(event,s)if not canMutate()then feedback=\"Recipe import requires operator access\";return end local transfers=event and event.getFiles and event.getFiles()or nil if type(transfers)~=\"table\"or#transfers~=1 then feedback=\"Drop exactly one recipe pack into Crafting Queue\";return end local transfer,chunks,total=transfers[1],{},0 if not transfer or type(transfer.read)~=\"function\"then feedback=\"Dropped file is not readable\";return end while true do local chunk=transfer.read(4096)if not chunk then break end if type(chunk)~=\"string\"then feedback=\"Dropped recipe pack has invalid data\";return end total=total+#chunk if total>crafting.RECIPE_PACK_MAX_BYTES then feedback=\"Recipe pack exceeds the 64 KiB import limit\";return end chunks[#chunks+1]=chunk end local pack,packErr=crafting.parseRecipePack(table.concat(chunks))if not pack then feedback=\"Recipe pack rejected: \"..tostring(packErr);recordRecipeImport({at=os.epoch(\"utc\"),source=transfer.getName and transfer.getName()or\"dropped file\",status=\"rejected\",error=tostring(packErr):sub(1,120)});return end local overlay,overlayErr=currentOverlay(s)if not overlay then feedback=\"Could not read local recipe overlay: \"..tostring(overlayErr);return end local review,reviewErr=crafting.reviewRecipePack(s.recipes or{},pack.recipes)if not review then feedback=\"Recipe pack rejected: \"..tostring(reviewErr);return end importReview={source=tostring(transfer.getName and transfer.getName()or\"recipe pack\"):sub(1,96),overlay=overlay,incomingTags=pack.tags or{},review=review,selected=1}importScroll,importArmed,feedback=0,nil,nil end local function importSelection(row)if row and(row.status==\"new\"or row.status==\"conflict\")then row.selected=not row.selected end end local function applyImportAction(action)if not importReview then return end local rows=importReview.review.rows if action==\"all_new\"then for _,row in ipairs(rows)do if row.status==\"new\"then row.selected=true end end elseif action==\"keep_conflicts\"then for _,row in ipairs(rows)do if row.status==\"conflict\"then row.selected=false end end elseif action==\"replace_conflicts\"then for _,row in ipairs(rows)do if row.status==\"conflict\"then row.selected=true end end elseif action==\"cancel_import\"then recordRecipeImport({at=os.epoch(\"utc\"),source=importReview.source,status=\"cancelled\",counts=importReview.review.counts})importReview=nil elseif action==\"apply_import\"then local merged,importedOrErr,tags=crafting.mergeRecipePack(importReview.overlay.recipes,importReview.review,importReview.overlay.tags,importReview.incomingTags)if not merged then feedback=\"Recipe import rejected: \"..tostring(importedOrErr);return end local saved,saveErr=crafting.writeRecipeFile(RECIPE_FILE,merged,tags)if not saved then feedback=\"Could not save recipes: \"..tostring(saveErr);return end recordRecipeImport({at=os.epoch(\"utc\"),source=importReview.source,status=\"imported\",imported=importedOrErr,counts=importReview.review.counts})importReview,feedback=nil,nil end end local function finishModal(s,rows)local kind,value=modal.kind,modal.value;modal=nil if kind==\"search\"then query=value:sub(1,64)queueCommand(\"catalog\",{query=query,offset=0},\"Recipe search\")elseif kind==\"destination\"and value~=\"\"then queueCommand(\"destination\",{value=value},\"Order address\")elseif kind==\"add\"then local n=tonumber(value);if n and n>0 and rows[selectedStock]then queueCommand(\"add\",{name=rows[selectedStock].name,count=n},\"Add\")end elseif kind==\"edit\"then local n=tonumber(value);if n and n>0 and s.cart[selectedCart]then queueCommand(\"edit\",{index=selectedCart,count=n},\"Edit\")end elseif kind==\"cancel_confirm\"then queueCommand(\"cancel_active\",{},\"Cancel craft\")elseif kind==\"clear_cart_confirm\"then queueCommand(\"clear_cart\",{},\"Clear request list\")end end local function sourceChoices(s,target)local choices={}if target==\"requester\"then choices[#choices+1]={label=\"No Redstone Requester\",value=false}end local key=target==\"recipes\"and\"recipeServers\"or(target..\"s\")for _,source in ipairs(((s.sources or{})[key]or{}))do choices[#choices+1]={label=tostring(source.label or((source.transport==\"peer\"and\"Peer \"..tostring(source.peer)..\" / \")or\"Local / \")..tostring(source.name)),value=source}end return choices end local function chooseSource(s,target)local choices,selected=sourceChoices(s,target),1 if#choices==0 then return end while true do local w,h=term.getSize()term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear()term.setBackgroundColor(colors.blue);term.setCursorPos(1,1);term.clearLine();term.setCursorPos(2,1);term.write(\"CeetOS Crafting Queue | Select \"..target)term.setTextColor(colors.lightGray);term.setCursorPos(2,3);term.write(\"Use arrows/Enter or click a source. Backspace cancels.\")local targets={}for index,choice in ipairs(choices)do local y=4+index-1;if y>=h then break end term.setBackgroundColor(index==selected and colors.gray or colors.black);term.setTextColor(index==selected and colors.white or colors.lightGray);term.setCursorPos(2,y);term.clearLine();term.write(choice.label:sub(1,w-3))targets[#targets+1]={y=y,index=index}end local event,a,b,c=os.pullEvent()if event==\"key\"then local key=navigationKey(a)if key==\"backspace\"or key==\"q\"then return elseif key==\"up\"then selected=math.max(1,selected-1)elseif key==\"down\"then selected=math.min(#choices,selected+1)elseif key==\"enter\"then queueCommand(\"source\",{target=target,value=choices[selected].value},target..\" source\");return end elseif event==\"mouse_click\"and a==1 then for _,hit in ipairs(targets)do if c==hit.y then selected=hit.index;break end end elseif event==\"mouse_up\"and a==1 then for _,hit in ipairs(targets)do if c==hit.y then queueCommand(\"source\",{target=target,value=choices[hit.index].value},target..\" source\");return end end end end end local function chooseTagConcrete(row)if not row or not row.craftable then return end queueCommand(\"tag_options\",{item=row.name},\"Tag choices\")local stage,selectedTag,selectedMember,timer=\"tags\",1,1,os.startTimer(2)while true do local s=state();cleanPending(s)local data=s.tagOptions local w,h=term.getSize()term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear()if w<36 or h<12 then terminalUi.minimum(\"Tagged ingredients\",\"Tag choices need 36x12\",\"Resize | Backspace\")else terminalUi.header(\"CeetOS Crafting Queue | Ingredient choices\",data and data.pending and 0.35 or 0)terminalUi.write(2,3,\"Recipe: \"..clipped(row.name,w-10),colors.lightGray,colors.black,w-3)local targets={}if not data or data.item~=row.name or data.pending then terminalUi.write(2,5,\"Loading valid concrete items from Recipe Server...\",colors.yellow,colors.black,w-3)elseif data.error then terminalUi.write(2,5,clipped(\"Cannot load choices: \"..tostring(data.error),w-3),colors.red,colors.black,w-3)elseif#(data.tags or{})==0 then terminalUi.write(2,5,\"This recipe has no tagged ingredients.\",colors.lightGray,colors.black,w-3)elseif stage==\"tags\"then terminalUi.write(2,5,\"Choose a tag to override. Auto uses the highest stock count.\",colors.lightGray,colors.black,w-3)for index,tag in ipairs(data.tags)do local y=6+index-1;if y>=h-1 then break end local current=(((s.tagChoices or{})[row.name]or{})[tag.tag])or tag.default local text=tag.tag..\" -> \"..crafting.displayRow(current,0,math.max(8,w-#tag.tag-8)):gsub(\" x0$\",\"\")terminalUi.write(3,y,clipped(text,w-5),index==selectedTag and colors.white or colors.lightGray,index==selectedTag and colors.gray or colors.black,w-5)targets[#targets+1]={x1=2,x2=w-1,y1=y,y2=y,index=index}end else local tag=data.tags[selectedTag]local options=tag and tag.options or{}terminalUi.write(2,5,clipped((tag and tag.tag or\"Tag\")..\" - choose concrete item\",w-3),colors.lightBlue,colors.black,w-3)local override=(((s.tagChoices or{})[row.name]or{})[tag.tag])local auto=\"Auto (highest stock) -> \"..crafting.displayName(tag.default)terminalUi.write(3,6,clipped(auto,w-5),selectedMember==1 and colors.white or colors.lightGray,selectedMember==1 and colors.gray or colors.black,w-5)targets[#targets+1]={x1=2,x2=w-1,y1=6,y2=6,member=false}for index,option in ipairs(options)do local y=6+index;if y>=h-1 then break end local marker=option.name==override and\"* \"or\"  \"terminalUi.write(3,y,clipped(marker..crafting.displayRow(option.name,option.count,w-5),w-5),selectedMember==index+1 and colors.white or colors.lightGray,selectedMember==index+1 and colors.gray or colors.black,w-5)targets[#targets+1]={x1=2,x2=w-1,y1=y,y2=y,member=option.name}end end terminalUi.write(2,h,stage==\"tags\"and\"Arrows select | Enter choose tag | Backspace return\"or\"Arrows select | Enter set choice | Backspace tags\",colors.gray,colors.black,w-3)local event,a,b,c=os.pullEvent()if event==\"timer\"and a==timer then stateDirty=true;timer=os.startTimer(2)elseif craftingStateChanged(event,a,b)then stateDirty=true elseif event==\"key\"then local key=navigationKey(a)if key==\"backspace\"or key==\"q\"then if stage==\"members\"then stage=\"tags\"else return end elseif data and data.tags and#data.tags>0 then if stage==\"tags\"then if key==\"up\"then selectedTag=math.max(1,selectedTag-1)elseif key==\"down\"then selectedTag=math.min(#data.tags,selectedTag+1)elseif key==\"enter\"then stage,selectedMember=\"members\",1 end else local total=#(data.tags[selectedTag].options or{})+1 if key==\"up\"then selectedMember=math.max(1,selectedMember-1)elseif key==\"down\"then selectedMember=math.min(total,selectedMember+1)elseif key==\"enter\"then local choice=selectedMember==1 and false or data.tags[selectedTag].options[selectedMember-1].name queueCommand(\"tag_choice\",{item=row.name,tag=data.tags[selectedTag].tag,value=choice},\"Ingredient choice\")return end end end elseif event==\"mouse_click\"and a==1 then for _,hit in ipairs(targets)do if c>=hit.y1 and c<=hit.y2 and b>=hit.x1 and b<=hit.x2 then if stage==\"tags\"then selectedTag=hit.index else queueCommand(\"tag_choice\",{item=row.name,tag=data.tags[selectedTag].tag,value=hit.member},\"Ingredient choice\")return end break end end end end end end local function applyAction(action,s,rows)if action==\"back\"then return\"exit\"end if action==\"menu\"then actionMenu={selected=1,scroll=0};return end if not canMutate()and action~=\"search\"and action~=\"clear_search\"and action~=\"filter\"and action~=\"refresh\"and action~=\"next_page\"and action~=\"previous_page\"then feedback=\"This action requires an operator session\"return end if action==\"search\"then startModal(\"search\",\"Find ticker items\",query)elseif action==\"clear_search\"then query=\"\";queueCommand(\"catalog\",{query=\"\",offset=0},\"Clear recipe search\")elseif action==\"filter\"then filter=filter==\"all\"and\"recipes\"or(filter==\"recipes\"and\"stocked\"or\"all\")queueCommand(\"catalog\",{query=query,offset=0},\"Recipe list\")elseif action==\"next_page\"or action==\"previous_page\"then local catalog,offset=type(s.recipeCatalog)==\"table\"and s.recipeCatalog or{},0 offset=math.max(0,math.floor(tonumber(catalog.offset)or 0))local pageSize=math.max(1,#(catalog.rows or{}))local nextOffset=action==\"next_page\"and(offset+pageSize)or math.max(0,offset-pageSize)if nextOffset~=offset and(action==\"previous_page\"or catalog.truncated==true)then selectedStock,stockScroll=1,0 queueCommand(\"catalog\",{query=query,offset=nextOffset},action==\"next_page\"and\"Next recipe page\"or\"Previous recipe page\")end elseif action==\"refresh\"then queueCommand(\"refresh\",{},\"Refresh\")elseif action==\"mode\"then if(s.config or{}).requester then queueCommand(\"mode\",{value=not s.craftMode},\"Craft mode\")end elseif action==\"destination\"then startModal(\"destination\",\"Order address\",(s.config or{}).orderAddress or\"Order\")elseif action==\"sources\"then chooseSource(s,\"ticker\");s=state();chooseSource(s,\"requester\");s=state();chooseSource(s,\"recipes\")elseif action==\"tags\"and focus==\"stock\"and rows[selectedStock]and rows[selectedStock].craftable then return\"tags\"elseif action==\"submit\"then queueCommand(\"submit\",{},\"Submit\")elseif action==\"retry\"and#(s.retries or{})>0 then queueCommand(\"retry\",{index=1},\"Retry\")elseif action==\"cancel\"and s.active then startModal(\"cancel_confirm\",\"Cancel active craft? Issued Create packages may still arrive\",\"\")elseif action==\"delete\"and focus==\"cart\"then queueCommand(\"remove\",{index=selectedCart},\"Remove\")elseif action==\"clear_cart\"and#(s.cart or{})>0 then startModal(\"clear_cart_confirm\",\"Clear every item from the request list?\",\"\")elseif action==\"add\"and focus==\"stock\"and rows[selectedStock]then startModal(\"add\",\"Add \"..rows[selectedStock].name..\" quantity\",\"1\",true)elseif action==\"edit\"and focus==\"cart\"and s.cart[selectedCart]then startModal(\"edit\",\"Set \"..s.cart[selectedCart].name..\" quantity\",s.cart[selectedCart].count,true)end end local function frame(x1,y1,x2,y2,title)return terminalUi.box(x1,y1,x2,y2,title,colors.gray,colors.lightBlue)end local function drawButton(x,y,entry,selected,armed,targets)local text=\"[\"..entry.label..\"]\"local width=select(1,term.getSize())if x+#text-1>width then return x end local background=armed and colors.yellow or(selected and colors.gray or colors.black)local enabled=canMutate()or entry.action==\"search\"or entry.action==\"filter\"or entry.action==\"refresh\"or entry.action==\"back\"terminalUi.write(x,y,text,armed and colors.black or(enabled and(selected and colors.white or colors.lightGray)or colors.gray),background)targets[#targets+1]={x1=x,x2=x+#text-1,y1=y,y2=y,action=entry.action}return x+#text+1 end local function clampScroll(selected,scroll,total,capacity)selected,total,capacity=math.max(1,selected or 1),math.max(0,total or 0),math.max(1,capacity or 1)if total==0 then return 1,0 end selected=math.min(total,selected)if selected<=scroll then scroll=selected-1 end if selected>scroll+capacity then scroll=selected-capacity end return selected,math.max(0,math.min(math.max(0,total-capacity),scroll or 0))end local function drawFooter(w,h,targets)terminalUi.clearLine(h-2,colors.black);terminalUi.clearLine(h-1,colors.black);terminalUi.clearLine(h,colors.black)terminalUi.write(2,h-2,\"Browse with arrows/WASD or mouse wheel. M opens every action.\",colors.gray,colors.black,w-3)local primary={{label=\"Actions M\",action=\"menu\"},{label=\"Send O\",action=\"submit\"},{label=\"Tags T\",action=\"tags\"},{label=\"Dest G\",action=\"destination\"},{label=\"Back\",action=\"back\"},}local x=2 for _,entry in ipairs(primary)do x=drawButton(x,h-1,entry,false,armedAction==entry.action,targets)if x>=w-7 then break end end terminalUi.write(2,h,\"Tab pane | Enter add/edit | PgUp/PgDn page | Backspace desktop\",colors.gray,colors.black,w-3)end local function drawActionMenu(w,h,targets)if not actionMenu then return end local x1,y1,x2,y2=2,2,w-1,h-2 frame(x1,y1,x2,y2,\"Actions\")local capacity=math.max(1,y2-y1-2)actionMenu.selected,actionMenu.scroll=clampScroll(actionMenu.selected,actionMenu.scroll,#actions,capacity)for offset=1,capacity do local index,entry=actionMenu.scroll+offset,actions[actionMenu.scroll+offset]if entry then local y=y1+offset local enabled=canMutate()or entry.action==\"search\"or entry.action==\"clear_search\"or entry.action==\"filter\"or entry.action==\"refresh\"or entry.action==\"next_page\"or entry.action==\"previous_page\"or entry.action==\"back\"terminalUi.write(x1+2,y,\"[\"..entry.label..\"]\",enabled and(index==actionMenu.selected and colors.white or colors.lightGray)or colors.gray,index==actionMenu.selected and colors.gray or colors.black,x2-x1-3)targets[#targets+1]={x1=x1+1,x2=x2-1,y1=y,y2=y,action=entry.action,menuIndex=index}end end terminalUi.write(x1+2,y2-1,\"Arrows/WASD | Enter select | Backspace close\",colors.gray,colors.black,x2-x1-3)end local function drawModal(w,h)if not modal then return end terminalUi.clearLine(h-2,colors.gray);terminalUi.write(2,h-2,modal.label,colors.white,colors.gray,w-3)if modal.kind==\"cancel_confirm\"or modal.kind==\"clear_cart_confirm\"then local buttons=modal.kind==\"clear_cart_confirm\"and\"[Clear request list] [Keep items]\"or\"[Confirm cancel] [Keep running]\"terminalUi.clearLine(h-1,colors.gray);terminalUi.write(2,h-1,buttons,colors.white,colors.gray,w-3)else terminalUi.clearLine(h-1,colors.gray)if modal.replaceOnInput then terminalUi.write(2,h-1,modal.value,colors.black,colors.lightBlue,w-4)else terminalUi.write(2,h-1,\"> \"..modal.value,colors.white,colors.gray,w-4)end term.setCursorPos(math.min(w-1,4+#modal.value),h-1)terminalUi.clearLine(h,colors.gray);terminalUi.write(2,h,\"[Save] [Cancel]\",colors.white,colors.gray,w-3)end end local function drawImportReview()local w,h=term.getSize()term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear()if w<36 or h<15 then terminalUi.minimum(\"Recipe import\",\"Recipe review needs 36x15\",\"Resize | Backspace\")return{targets={},rows={}}end local review,counts=importReview.review,importReview.review.counts terminalUi.header(\"CeetOS Crafting Queue | Review recipe pack\",0.35)terminalUi.write(2,3,\"File: \"..clipped(importReview.source,w-8),colors.lightGray,colors.black,w-3)terminalUi.write(2,4,(\"New: %d  Conflicts: %d  Unchanged: %d  Invalid: %d\"):format(counts.new or 0,counts.conflict or 0,counts.unchanged or 0,counts.invalid or 0),colors.white,colors.black,w-3)frame(1,5,w,h-4,\"Recipes\")local capacity=math.max(1,h-10)importReview.selected=math.max(1,math.min(math.max(1,#review.rows),importReview.selected or 1))if importReview.selected<=importScroll then importScroll=importReview.selected-1 end if importReview.selected>importScroll+capacity then importScroll=importReview.selected-capacity end importScroll=math.max(0,math.min(math.max(0,#review.rows-capacity),importScroll))local targets,visible={},{}for offset=1,capacity do local index,row,y=importScroll+offset,review.rows[importScroll+offset],5+offset if row then local marker=row.status==\"new\"and(row.selected and\"[+]\"or\"[ ]\")or row.status==\"conflict\"and(row.selected and\"[Replace]\"or\"[Keep]\")or\"[Same]\"local colour=row.status==\"new\"and colors.lime or row.status==\"conflict\"and colors.yellow or colors.gray terminalUi.write(3,y,marker..\" \"..row.name,colour,index==importReview.selected and colors.gray or colors.black,w-5)targets[#targets+1]={x1=2,x2=w-1,y1=y,y2=y,action=\"select_recipe\",index=index}visible[#visible+1]=row end end local selected=review.rows[importReview.selected]if selected then local localText=selected.existing and(\"Local: \"..tostring(selected.existing.output)..\" via \"..tostring(selected.existing.address))or\"Local: none\"local incomingText=\"Incoming: \"..tostring(selected.incoming.output)..\" via \"..tostring(selected.incoming.address)terminalUi.write(2,h-3,clipped(localText..\" | \"..incomingText,w-3),colors.lightGray,colors.black,w-3)end local buttons={{label=\"Import\",action=\"apply_import\"},{label=\"All new\",action=\"all_new\"},{label=\"Keep conflicts\",action=\"keep_conflicts\"},{label=\"Replace conflicts\",action=\"replace_conflicts\"},{label=\"Cancel\",action=\"cancel_import\"},}local x,buttonRow=2,h-2 for _,button in ipairs(buttons)do local text=\"[\"..button.label..\"]\"if x+#text-1>w-1 and buttonRow==h-2 then x,buttonRow=2,h-1 end if x+#text-1<=w-1 then terminalUi.write(x,buttonRow,text,importArmed==button.action and colors.black or colors.white,importArmed==button.action and colors.yellow or colors.gray)targets[#targets+1]={x1=x,x2=x+#text-1,y1=buttonRow,y2=buttonRow,action=button.action}x=x+#text+1 end end terminalUi.write(2,h,\"Arrows select | Space toggle | Enter import | A/K/X | Backspace cancel\",colors.gray,colors.black,w-3)return{targets=targets,rows=visible}end local function draw(s,rows)local w,h=term.getSize();term.setBackgroundColor(colors.black);term.setTextColor(colors.white);term.clear()if w<36 or h<15 then return{targets=terminalUi.minimum(\"CeetOS Crafting Queue\",\"Crafting Queue needs 36x15\",\"Resize | Backspace\")or{},compact=true,actions={}}end local config=s.config or{}local tickerLabel=config.ticker and tostring(config.ticker.label or config.ticker.name)or\"not selected\"local requesterLabel=config.requester and tostring(config.requester.label or config.requester.name)or\"none\"local recipeLabel=config.recipeServer and tostring(config.recipeServer.label or(\"Peer \"..tostring(config.recipeServer.peer)))or\"local\"local pendingActive=next(pending)~=nil local progress=s.active and 0.65 or(pendingActive and 0.25 or 0)terminalUi.header(\"CeetOS Crafting Queue | Ticker: \"..tickerLabel..\" | Recipes: \"..recipeLabel,progress)local items,total=crafting.cartTotals(s.cart)frame(1,2,w,5,\"Order\")local targets={}local destinationText=\"Destination: \"..tostring(config.orderAddress or\"Order\")..\"  \"local modeText=\"[Craft: \"..(s.craftMode and\"ON\"or\"OFF\")..\"]\"terminalUi.write(3,3,destinationText,colors.white,colors.black,w-5)local modeX=3+#destinationText local modeEnabled=config.requester~=nil and canMutate()if modeX+#modeText<=w-2 then terminalUi.write(modeX,3,modeText,modeEnabled and(s.craftMode and colors.lime or colors.lightGray)or colors.gray,colors.black)if modeEnabled then targets[#targets+1]={x1=modeX,x2=modeX+#modeText-1,y1=3,y2=3,action=\"mode\"}end elseif not config.requester then terminalUi.write(3,3,\"Craft mode unavailable: select a requester\",colors.gray,colors.black,w-5)end local field=\"[\"..clipped(query==\"\"and\"Search inventory / recipes\"or query,math.max(12,math.floor(w*.42)))..\"]\"terminalUi.write(3,4,field,colors.white,colors.gray,math.max(1,#field))targets[#targets+1]={x1=3,x2=math.min(w-2,2+#field),y1=4,y2=4,action=\"search\"}local x=4+#field if x+6<w-15 then terminalUi.write(x,4,\"[Clear]\",colors.lightGray,colors.black)targets[#targets+1]={x1=x,x2=x+6,y1=4,y2=4,action=\"clear_search\"}x=x+8 end local filterLabel=filter==\"recipes\"and\"Recipes\"or(filter==\"stocked\"and\"Stock\"or\"All\")terminalUi.write(x,4,\"View: [\"..filterLabel..\"]\",colors.lightGray,colors.black,math.max(1,w-x-2))targets[#targets+1]={x1=x,x2=w-2,y1=4,y2=4,action=\"filter\"}local active,status=s.active,pendingText()or\"Ready\"if active then local item=active.items and active.items[active.index]if active.phase==\"await_crafted_output\"or active.phase==\"await_craft_step_output\"then local route=active.stepRoute or\"recipe route\"local dispatched=tonumber(active.craftIssued)or 0 local suffix=active.phase==\"await_craft_step_output\"and(\" after \"..tostring(dispatched)..\" requester dispatch\"..(dispatched==1 and\"\"or\"es\"))or\"\"status=(\"%s; waiting for +%s %s in Stock Ticker%s\"):format(route,tostring(active.expectedOutput or\"?\"),tostring(active.waitingFor or item and item.name or\"recipe\"),suffix)elseif active.phase==\"craft\"and active.pendingCraftRequests then status=(\"Requester batching %s; %s sent, %s craft request%s remaining\"):format(tostring(item and item.name or\"recipe\"),tostring(active.craftIssued or 0),tostring(active.pendingCraftRequests),active.pendingCraftRequests==1 and\"\"or\"s\")elseif active.phase==\"await_package_confirmation\"or active.phase==\"await_peer_package\"then status=(\"Stock Ticker -> %s; package %s/%s: waiting for %s\"):format(tostring(active.stepAddress or\"destination\"),tostring(active.packageIndex or 1),tostring(active.packageCount or\"?\"),tostring(active.waitingFor or\"ingredient\"))elseif active.phase==\"await_delivery_confirmation\"then status=(\"Order submitted to %s; waiting for ticker decrease of %s\"):format(tostring(active.destination),tostring(active.accepted or\"?\"))else status=(\"%s | %s  requested:%s accepted:%s dispatched:%s remaining:%s\"):format(tostring(active.phase),tostring(item and item.name or\"finishing\"),tostring(active.requested or(item and item.count)or 0),tostring(active.accepted or\"-\"),tostring(active.delivered or 0),tostring(active.remaining or\"-\"))end end frame(1,6,w,8,active and\"Active job\"or\"Status\")local catalog=type(s.recipeCatalog)==\"table\"and s.recipeCatalog or{}local foregroundError=s.error or feedback or catalog.error if not active and catalog.pending then status=\"Loading registered recipes...\"end terminalUi.write(3,7,foregroundError or status,foregroundError and colors.red or(active and colors.yellow or colors.lightGray),colors.black,w-5)local split,top,bottom=math.max(24,math.floor(w*.57)),10,h-4 local lines=math.max(1,bottom-top)selectedStock,stockScroll=clampScroll(selectedStock,stockScroll,#rows,lines)selectedCart,cartScroll=clampScroll(selectedCart,cartScroll,#(s.cart or{}),lines)local stockFirst,stockLast=#rows==0 and 0 or stockScroll+1,math.min(#rows,stockScroll+lines)local cartFirst,cartLast=#(s.cart or{})==0 and 0 or cartScroll+1,math.min(#(s.cart or{}),cartScroll+lines)local shown=stockFirst==0 and\"empty\"or(tostring(stockFirst)..\"-\"..tostring(stockLast)..\"/\"..tostring(#rows))frame(1,9,split,bottom,\"Items & recipes (\"..tostring(catalog.total or 0)..\") \"..shown)frame(split+1,9,w,bottom,\"Request list\")for i=1,lines do local stockIndex,cartIndex=stockScroll+i,cartScroll+i local y,item,cart=top+i-1,rows[stockIndex],(s.cart or{})[cartIndex]if item then terminalUi.write(3,y,crafting.displayRow(item.name,item.count,split-4),item.craftable and colors.lime or colors.white,focus==\"stock\"and selectedStock==stockIndex and colors.gray or colors.black,split-4)end if cart then terminalUi.write(split+3,y,crafting.displayRow(cart.name,cart.count,w-split-4),colors.white,focus==\"cart\"and selectedCart==cartIndex and colors.gray or colors.black,w-split-4)end end local history,retry=(s.history or{})[#(s.history or{})],(s.retries or{})[1]local detail=s.recoveryNotice or(history and(\"Last: \"..tostring(history.status)..\" \"..tostring(history.name or\"job\")..\" \"..tostring(history.dispatched or history.delivered or 0)..\"/\"..tostring(history.requested or 0)..(history.error and(\" - \"..tostring(history.error))or\"\"))or\"No dispatched orders yet\")if retry then detail=detail..\" | Retry: \"..tostring(retry.items[1].name)..\" x\"..tostring(retry.items[1].count)end terminalUi.write(2,bottom+1,detail,colors.gray,colors.black,w-2)drawFooter(w,h,targets)if modal then drawModal(w,h)end drawActionMenu(w,h,targets)return{split=split,top=top,bottom=bottom-1,targets=targets,stockScroll=stockScroll,cartScroll=cartScroll,stockCount=#rows,cartCount=#(s.cart or{})}end local function hit(targets,x,y)return terminalUi.hit(targets,x,y)end local function renderKey(s,rows)local w,h=term.getSize()local serialised=textutils.serialise({width=w,height=h,state={stock=s.stock,cart=s.cart,active=s.active,retries=s.retries,history=s.history,error=s.error,recoveryNotice=s.recoveryNotice,craftMode=s.craftMode,config=s.config,recipeCatalog=s.recipeCatalog,acknowledgements=s.acknowledgements,tagChoices=s.tagChoices},rows=rows,query=query,filter=filter,focus=focus,selectedStock=selectedStock,selectedCart=selectedCart,stockScroll=stockScroll,cartScroll=cartScroll,modal=modal,actionMenu=actionMenu,pending=pendingText(),armed=armedAction,importReview=importReview,importScroll=importScroll,importArmed=importArmed,},{compact=true})return serialised end local timer,renderedKey,layout=os.startTimer(2),nil,nil while true do local s=state();cleanPending(s)local rows=crafting.rows(s.stock,query,filter,s.recipes,s.recipeCatalog)selectedStock=math.max(1,math.min(math.max(1,#rows),selectedStock));selectedCart=math.max(1,math.min(math.max(1,#s.cart),selectedCart))local key=renderKey(s,rows)if key~=renderedKey or not layout then layout=importReview and drawImportReview()or draw(s,rows)renderedKey=key end local event,a,b,c=os.pullEvent()if event==\"timer\"and a==timer then stateDirty=true;timer=os.startTimer(2)elseif craftingStateChanged(event,a,b)then stateDirty=true elseif importReview then if event==\"key\"then local key=navigationKey(a)if key==\"backspace\"or key==\"q\"then applyImportAction(\"cancel_import\")elseif key==\"up\"then importReview.selected=math.max(1,(importReview.selected or 1)-1)elseif key==\"down\"then importReview.selected=math.min(#importReview.review.rows,(importReview.selected or 1)+1)elseif key==\"space\"or key==\"left\"or key==\"right\"then importSelection(importReview.review.rows[importReview.selected])elseif key==\"a\"then applyImportAction(\"all_new\")elseif key==\"k\"then applyImportAction(\"keep_conflicts\")elseif key==\"x\"then applyImportAction(\"replace_conflicts\")elseif key==\"enter\"then applyImportAction(\"apply_import\")end elseif event==\"mouse_click\"and a==1 then local target=hit(layout.targets,b,c)if target and target.action==\"select_recipe\"then importReview.selected=target.index elseif target then importArmed=target.action end elseif event==\"mouse_drag\"and a==1 then local target=hit(layout.targets,b,c);importArmed=target and target.action or nil elseif event==\"mouse_up\"and a==1 then local target=hit(layout.targets,b,c)if target and target.action==\"select_recipe\"then importReview.selected=target.index;importSelection(importReview.review.rows[target.index])elseif target and target.action==importArmed then applyImportAction(target.action)end importArmed=nil end elseif modal then local confirmation=modal.kind==\"cancel_confirm\"or modal.kind==\"clear_cart_confirm\"if event==\"char\"and not confirmation then modal.value=(modal.replaceOnInput and\"\"or modal.value)..a;modal.replaceOnInput=false elseif event==\"paste\"and not confirmation then modal.value=(modal.replaceOnInput and\"\"or modal.value)..tostring(a):gsub(\"[\\r\\n]\",\"\");modal.replaceOnInput=false elseif event==\"key\"then local key=navigationKey(a)if key==\"enter\"then finishModal(s,rows)elseif key==\"backspace\"then if confirmation or#modal.value==0 then modal=nil else modal.value=modal.value:sub(1,-2);modal.replaceOnInput=false end end elseif event==\"mouse_up\"and a==1 then local _,h=term.getSize()if confirmation then if c==h-1 and b>=2 and b<=21 then finishModal(s,rows)elseif c==h-1 then modal=nil end elseif c==h and b>=2 and b<=7 then finishModal(s,rows)elseif c==h and b>=9 then modal=nil end end elseif actionMenu then if event==\"key\"then local key=navigationKey(a)if key==\"backspace\"or key==\"q\"or key==\"m\"then actionMenu=nil elseif key==\"up\"then actionMenu.selected=math.max(1,actionMenu.selected-1)elseif key==\"down\"then actionMenu.selected=math.min(#actions,actionMenu.selected+1)elseif key==\"pageup\"then actionMenu.selected=math.max(1,actionMenu.selected-8)elseif key==\"pagedown\"then actionMenu.selected=math.min(#actions,actionMenu.selected+8)elseif key==\"enter\"then local action=actions[actionMenu.selected]and actions[actionMenu.selected].action actionMenu=nil if action then local outcome=applyAction(action,s,rows)if outcome==\"exit\"then return elseif outcome==\"tags\"then chooseTagConcrete(rows[selectedStock])end end end elseif event==\"mouse_click\"and a==1 then local target=hit(layout.targets,b,c)if target and target.menuIndex then actionMenu.selected,armedAction=target.menuIndex,target.action end elseif event==\"mouse_drag\"and a==1 then local target=hit(layout.targets,b,c);armedAction=target and target.action or nil elseif event==\"mouse_up\"and a==1 then local target=hit(layout.targets,b,c)if target and target.menuIndex and target.action==armedAction then actionMenu=nil local outcome=applyAction(target.action,s,rows)if outcome==\"exit\"then return elseif outcome==\"tags\"then chooseTagConcrete(rows[selectedStock])end end armedAction=nil elseif event==\"mouse_scroll\"then actionMenu.selected=math.max(1,math.min(#actions,actionMenu.selected+(tonumber(a)or 0)))end elseif event==\"key\"then local key=navigationKey(a)if key==\"backspace\"or key==\"q\"then return elseif key==\"tab\"then focus=focus==\"stock\"and\"cart\"or\"stock\"elseif key==\"up\"then if focus==\"stock\"then local catalog=type(s.recipeCatalog)==\"table\"and s.recipeCatalog or{}if selectedStock<=1 and(tonumber(catalog.offset)or 0)>0 then applyAction(\"previous_page\",s,rows)else selectedStock=math.max(1,selectedStock-1)end elseif focus==\"cart\"then selectedCart=math.max(1,selectedCart-1)end elseif key==\"down\"then if focus==\"stock\"then if#rows>0 and selectedStock>=#rows then applyAction(\"next_page\",s,rows)else selectedStock=math.min(math.max(1,#rows),selectedStock+1)end elseif focus==\"cart\"then selectedCart=math.min(math.max(1,#s.cart),selectedCart+1)end elseif key==\"pageup\"then if focus==\"stock\"then selectedStock=math.max(1,selectedStock-math.max(1,layout.bottom-layout.top))else selectedCart=math.max(1,selectedCart-math.max(1,layout.bottom-layout.top))end elseif key==\"pagedown\"then if focus==\"stock\"then selectedStock=math.min(math.max(1,#rows),selectedStock+math.max(1,layout.bottom-layout.top))else selectedCart=math.min(math.max(1,#s.cart),selectedCart+math.max(1,layout.bottom-layout.top))end elseif key==\"home\"then if focus==\"stock\"then selectedStock=1 else selectedCart=1 end elseif key==\"end\"then if focus==\"stock\"then selectedStock=math.max(1,#rows)else selectedCart=math.max(1,#s.cart)end elseif key==\"s\"then applyAction(\"search\",s,rows)elseif key==\"f\"then applyAction(\"filter\",s,rows)elseif key==\"r\"then applyAction(\"refresh\",s,rows)elseif key==\"m\"then applyAction(\"menu\",s,rows)elseif key==\"space\"then applyAction(\"mode\",s,rows)elseif key==\"g\"then applyAction(\"destination\",s,rows)elseif key==\"t\"then chooseTagConcrete(rows[selectedStock])elseif key==\"o\"then applyAction(\"submit\",s,rows)elseif key==\"c\"then applyAction(\"cancel\",s,rows)elseif key==\"delete\"then applyAction(\"delete\",s,rows)elseif key==\"enter\"then local action=focus==\"stock\"and\"add\"or\"edit\"if action then local outcome=applyAction(action,s,rows)if outcome==\"exit\"then return elseif outcome==\"tags\"then chooseTagConcrete(rows[selectedStock])end end end elseif event==\"mouse_click\"and a==1 then local target=hit(layout.targets,b,c)if target then armedAction=target.action elseif c>=layout.top and c<=layout.bottom then local index=c-layout.top+1 if b<=layout.split and rows[(layout.stockScroll or 0)+index]then index=(layout.stockScroll or 0)+index focus,selectedStock=\"stock\",index local now=os.epoch(\"utc\")if lastRowClick and lastRowClick.side==\"stock\"and lastRowClick.index==index and now-lastRowClick.at<=450 then startModal(\"add\",\"Add \"..rows[index].name..\" quantity\",\"1\",true);lastRowClick=nil else lastRowClick={side=\"stock\",index=index,at=now}end elseif b>layout.split and s.cart[(layout.cartScroll or 0)+index]then index=(layout.cartScroll or 0)+index focus,selectedCart=\"cart\",index local now=os.epoch(\"utc\")if lastRowClick and lastRowClick.side==\"cart\"and lastRowClick.index==index and now-lastRowClick.at<=450 then startModal(\"edit\",\"Set \"..s.cart[index].name..\" quantity\",s.cart[index].count,true);lastRowClick=nil else lastRowClick={side=\"cart\",index=index,at=now}end end end elseif event==\"mouse_scroll\"then local delta=tonumber(a)or 0 if c>=layout.top and c<=layout.bottom then if b<=layout.split then local catalog=type(s.recipeCatalog)==\"table\"and s.recipeCatalog or{}if delta>0 and#rows>0 and selectedStock>=#rows then applyAction(\"next_page\",s,rows)elseif delta<0 and selectedStock<=1 and(tonumber(catalog.offset)or 0)>0 then applyAction(\"previous_page\",s,rows)else selectedStock=math.max(1,math.min(math.max(1,#rows),selectedStock+delta))end else selectedCart=math.max(1,math.min(math.max(1,#s.cart),selectedCart+delta))end end elseif event==\"mouse_drag\"and a==1 then local target=hit(layout.targets,b,c);armedAction=target and target.action or nil elseif event==\"mouse_up\"and a==1 then local target=hit(layout.targets,b,c)if target and target.action==armedAction then local outcome=applyAction(target.action,s,rows)if outcome==\"exit\"then return elseif outcome==\"tags\"then chooseTagConcrete(rows[selectedStock])end end armedAction=nil elseif event==\"file_transfer\"then feedback=\"Recipe packs must be imported on the selected Recipe Server\"end end",
  ["ceetos/bin/ceetshareall.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local auth=require(\"ceetos.lib.auth\")local peripherals=require(\"ceetos.lib.peripherals\")local mode=(...)==\"master\"and\"master\"or\"peer\"auth.require(\"operator\")print(\"Shared \"..peripherals.shareAll(mode)..\" peripherals (\"..mode..\")\")",
  ["ceetos/bin/ceetunshareall.lua"] = "package.path=\"/?.lua;/?/init.lua;\"..package.path local auth=require(\"ceetos.lib.auth\")local peripherals=require(\"ceetos.lib.peripherals\")auth.require(\"operator\")print(\"Unshared \"..peripherals.unshareAll()..\" peripherals\")",
}
local CEETOS_RELEASE_INSTALLER = true
local CEETOS_PROFILE = "desktop"
local CEETOS_VERSION = "0.19.50"
local CEETOS_DEV_BRIDGE_URL = nil

-- `wget --install` may run inside a deliberately restricted CeetShell child
-- environment. Self-cleaning is optional there: never require the shell API
-- merely to install the runtime.
local args = { ... }
local automated = args[1] == "--ceetos-apply"
local self = shell and shell.getRunningProgram and shell.getRunningProgram()
local cache = "/ceetos-updates/release.lua"
-- A signed release launched directly is retained outside the replaceable
-- runtime tree for verified recovery. Development installers deliberately do
-- not become propagation sources. Do this before removing the downloaded
-- program itself, but avoid copying the cache onto itself during recovery.
if CEETOS_RELEASE_INSTALLER and not automated and self and fs.exists(self) and self ~= cache then
  if not fs.exists("/ceetos-updates") then fs.makeDir("/ceetos-updates") end
  if fs.exists(cache) then fs.delete(cache) end
  pcall(fs.copy, self, cache)
end
if self and self ~= cache and fs.exists(self) then pcall(fs.delete, self) end

local bytes = 0
local fileCount = 0
for _, contents in pairs(files) do bytes = bytes + #contents; fileCount = fileCount + 1 end
print("CeetOS " .. CEETOS_PROFILE .. " v" .. CEETOS_VERSION .. " installer (" .. bytes .. " bytes)")
if not automated then
  print("This will replace /ceetos and /startup.lua. Continue? [y/N]")
  if read():lower() ~= "y" then print("Cancelled."); return end
end

local backup, migrate, migrationData = nil, false, {}
-- A signed release must remain independently installable. Broker enrollment
-- is an optional post-install Cloud setup path, never a prerequisite for a
-- fresh offline/public `wget` installation.
-- Clean stale full-tree backups from older installers before measuring space.
for _, name in ipairs(fs.list(".")) do
  if name:match("^ceetos%.backup") or name == "ceetos-migrate" then fs.delete(name) end
end
-- Development transactions can be much larger than the runtime. Preserve
-- only the reconnect record; staged payloads, test data, and rollback trees
-- are disposable and must not prevent an install from starting.
for _, name in ipairs({ "staging", "backups", "test-data" }) do
  local path = fs.combine("ceetos-dev", name)
  if fs.exists(path) then fs.delete(path) end
end
local hadRuntime = fs.exists("ceetos")
-- A fresh computer has no old runtime whose removal can make room. Check the
-- actual free space before touching its filesystem: `getCapacity` is only a
-- limit and does not prove that the writable mount has space left.
local free = fs.getFreeSpace("/")
local reserve = 8192 + fileCount * 1024
local required = bytes + reserve
if not hadRuntime and type(free) == "number" and free < required then
  printError("Not enough free space for CeetOS: " .. tostring(free) .. " bytes free; approximately " .. tostring(required) .. " bytes required.")
  return
end
if hadRuntime then
  if automated then migrate = true else
    print("Migrate saved CeetOS configuration? [Y/n]")
    local choice = read():lower()
    migrate = choice ~= "n" and choice ~= "no"
  end
  -- Copy only durable configuration to a small migration area, then remove
  -- the old tree before writing the new one. This works on nearly-full CC
  -- drives where retaining a complete backup would make installation fail.
  if migrate then
    local durable = { "users.lua", "network.lua", "shares.lua", "config.lua", "owner-service.lua", "seen-peers.lua", "jobs.lua", "job-workers.lua", "crafting-state.lua", "auth-authority.lua", "auth-authority-state.lua", "auth-directory.lua", "auth-session.lua", "cloud.lua", "cloud-state.lua" }
    if CEETOS_PROFILE == "recipe-server" then durable[#durable + 1] = "recipes.lua" end
    for _, name in ipairs(durable) do
      local source = fs.combine("ceetos/data", name)
      if fs.exists(source) then
        local input = fs.open(source, "r")
        if input then migrationData[name] = input.readAll(); input.close() end
      end
    end
  end
  fs.delete("ceetos")
end

local writingPath = nil
local wrote, writeErr = pcall(function()
  for path, contents in pairs(files) do
    writingPath = path
    local parent = fs.getDir(path)
    if parent ~= "" and not fs.exists(parent) then fs.makeDir(parent) end
    local handle = assert(fs.open(path, "w"))
    handle.write(contents)
    handle.close()
  end
end)
if not wrote then
  -- Fresh installs have no previous runtime to preserve. Remove incomplete
  -- output so a retry starts from a known-empty CeetOS tree.
  if not hadRuntime and fs.exists("ceetos") then fs.delete("ceetos") end
  printError("CeetOS installation failed while writing " .. tostring(writingPath or "installer data") .. ": " .. tostring(writeErr) .. ". Started with " .. tostring(free) .. " bytes free; needs approximately " .. tostring(required) .. " bytes.")
  return
end

-- Installing a profile is an explicit local conversion. A previous desktop
-- runtime may have left a durable profile override behind; if it wins over
-- the freshly installed declaration, a Recipe/Router/Auth Server would boot
-- and register with CeetDev as a desktop. Do not migrate that override:
-- replace it with the profile bound into this installer before startup.
if not fs.exists("ceetos/data") then fs.makeDir("ceetos/data") end
local profileOutput = assert(fs.open("ceetos/data/profile.lua", "w"))
profileOutput.write(textutils.serialise({ schema = 1, id = CEETOS_PROFILE, installedAt = os.epoch("utc") }))
profileOutput.close()

if migrate then
  -- Preserve durable configuration, but never preserve an active session,
  -- temporary recovery window, remote result cache, or stale audit entries.
  local durable = { "users.lua", "network.lua", "shares.lua", "config.lua", "owner-service.lua", "seen-peers.lua", "jobs.lua", "job-workers.lua", "crafting-state.lua", "auth-authority.lua", "auth-authority-state.lua", "auth-directory.lua", "auth-session.lua", "cloud.lua", "cloud-state.lua" }
  if CEETOS_PROFILE == "recipe-server" then durable[#durable + 1] = "recipes.lua" end
  for _, name in ipairs(durable) do
    local contents = migrationData[name]
    if contents then
      if not fs.exists("ceetos/data") then fs.makeDir("ceetos/data") end
      local output = assert(fs.open(fs.combine("ceetos/data", name), "w"))
      output.write(contents); output.close()
    end
  end
  print("Saved CeetOS configuration migrated. Please log in again.")
end

-- A DevBuild downloaded from this loopback bridge is an explicit local
-- developer action. Persist a short-lived, tokenless local connection so the
-- newly installed runtime registers after first boot without requiring a
-- second `ceetdev ws://...` command. Release installers and standalone
-- DevBuild artifacts deliberately have no embedded bridge URL.
local function localLoopback(url)
  return type(url) == "string" and (url:match("^ws://127%.0%.0%.1[:/]") or url:match("^ws://localhost[:/]") or url:match("^ws://%[::1%][:/]"))
end
local function writeValue(path, value)
  local parent, temporary = fs.getDir(path), path .. ".tmp"
  if parent ~= "" and not fs.exists(parent) then fs.makeDir(parent) end
  if fs.exists(temporary) then fs.delete(temporary) end
  local output = assert(fs.open(temporary, "w")); output.write(textutils.serialise(value)); output.close()
  if fs.exists(path) then fs.delete(path) end
  fs.move(temporary, path)
end
if not CEETOS_RELEASE_INSTALLER and localLoopback(CEETOS_DEV_BRIDGE_URL) then
  local existing = nil
  if fs.exists("/ceetos-dev/connection.lua") then
    local input = fs.open("/ceetos-dev/connection.lua", "r")
    if input then existing = textutils.unserialise(input.readAll()); input.close() end
  end
  -- Never replace an explicitly configured LAN/token bridge. A local
  -- loopback configuration is safe to refresh because it is bound to this
  -- same workstation and contains no credential.
  if type(existing) ~= "table" or localLoopback(existing.url) then
    writeValue("/ceetos-dev/connection.lua", { url = CEETOS_DEV_BRIDGE_URL, token = "", auth = "none", source = "devbuild-installer", autoConnect = true })
    writeValue("/ceetos-dev/recovery/developer-mode.lua", { schema = 1, enabledAt = os.epoch("utc"), untilAt = os.epoch("utc") + 8 * 60 * 60 * 1000, source = "local-installer" })
    print("Local DevBridge auto-connect enabled for this DevBuild.")
  end
end

-- The installer is intentionally transactional while it runs, but retaining
-- a full tree backup after a successful install quickly exhausts small CC
-- drives. Durable data has already been copied, so remove the old tree.
if automated then
  -- The root startup launcher sees this marker and boots the freshly written
  -- runtime rather than replaying the installer. The runtime verifies its
  -- loaded version before clearing it.
  if not fs.exists("/ceetos-updates") then fs.makeDir("/ceetos-updates") end
  local marker = fs.open("/ceetos-updates/pending.lua", "w")
  if marker then marker.write("return {state='verify',version=" .. "0.19.50" .. ",profile=" .. "\"desktop\"" .. "}"); marker.close() end
  print("CeetOS installed. Rebooting to verify the update.")
  os.reboot()
end
print("CeetOS installed. Reboot to start it.")

-- CEETOS_RELEASE_PAYLOAD_END
