<rss version="2.0">
  <channel>
    <title>Penn Mackintosh</title>
    <link>https://hack5.dev</link>
    <description>Penn Mackintosh - Blog</description>
    <generator>Zine -- https://zine-ssg.io</generator>
    <language>en-GB</language>
    <lastBuildDate>Sun, 01 Nov 2026 22:26:30 +0000</lastBuildDate>
    
      <item>
        <title>Make it fast, then make it right</title>
        <description>&lt;h2&gt;Abstract&lt;/h2&gt;&lt;p&gt;I challenge the common wisdom of “make it work, make it right, make it fast”. Sometimes, you have to make it fast, then make it work. When working on machine learning, you have to process huge amounts of data, and code errors are almost impossible to debug or even detect without analysing that data at scale. As with any software engineering task, bugs are fixed through repeated cycles of finding and fixing a bug, then moving on to the next one. If you write slow code, each cycle of bug-fixing (“make it right”) takes a long time. But if you first focus on the performance of the program (“make it fast”) and then start fixing bugs, the amount of time spent in the bug fixing phase could be lower. I present an anecdote with absolutely no statistical evidence whatsoever.&lt;/p&gt;&lt;h2&gt;Introduction&lt;/h2&gt;&lt;p&gt;I couldn’t possibly write a blog post today without mentioning LLMs in some way.&lt;/p&gt;&lt;p&gt;In today’s software engineering culture there seems to be a huge focus on churning out code as fast as possible. That means less review of the code, and perhaps more importantly, less thoughtful structuring. In all the “slop” codebases I’ve worked on, a common pattern is that the same task will be solved in three different ways in three different places. LLMs do love tricolon, after all. More different places solving the same problem means more chances that one of them has a bug, or worse, that you don’t even remember to fix the bug in all three at once! If you’re writing a data-intensive pipeline, with this less-structured code also comes less thought about the flow of data through the pipeline. While there is a good chance that the LLM approximates an existing efficient pipeline structure from training data, that’s unlikely to remain the case through many agent debugging cycles. Of course, the agent can detect when things are running slowly and try to optimise it, but that usually involves weird compiler-level hacks rather than thoughtful structure for efficiency. The end result is that by the time you, the human, need to debug the code, it’s a nonsensical mess that runs slowly. Much the same as if you had a team of undergraduates writing it, which is how I got into this situation.&lt;/p&gt;&lt;p&gt;For a coursework, we were asked to write a deep reinforcement learning agent. Not a particularly difficult task in general, but we chose an unusual problem space so we had a fair bit of reward shaping to do, and we weren’t allowed to use RL libraries. At the point where we were beginning to debug our broken code, I started trying to optimise our code, for example by using numpy functions rather than pure-python maths, and by batching operations together.&lt;/p&gt;&lt;p&gt;One team member said there was no point optimising until we finished fixing the bugs, and that’s what I’m refuting.&lt;/p&gt;&lt;h2&gt;My hypothesis&lt;/h2&gt;&lt;p&gt;For us to fix the bugs, we needed to run the full pipeline and check what was happening at each stage (often by looking at the standard deviation of various inputs and outputs to see if data was getting lost or rescaled). Then we needed to tweak each section, either by fixing a bug or tuning a hyperparameter.&lt;/p&gt;&lt;p&gt;I think that if we had not spent time optimising the pipeline early on, we would have taken much longer to debug it. Of course, there were overheads introduced by the optimisation, for instance when a numpy method gives an error it can often be much more obscure than the corresponding pure-Python messages. However, I suspect that it was worth it.&lt;/p&gt;&lt;h2&gt;That’s all&lt;/h2&gt;&lt;p&gt;I don’t think this post really deserves a conclusion since I didn’t do any research. I suppose you could call it inconclusive.&lt;/p&gt;</description>
        <link>https://hack5.dev/blog/make-it-fast/</link>
        <pubDate>Fri, 30 Oct 2026 10:30:00 +0000</pubDate>
        <guid>https://hack5.dev/blog/make-it-fast/</guid>
      </item>
    
      <item>
        <title>Fixing downloads in DecisionTime</title>
        <description>&lt;h2&gt;Summary&lt;/h2&gt;&lt;p&gt;When I try to download files from &lt;a href=&quot;https://www.oneadvanced.com/products/meetings-and-board-management/&quot;&gt;Decision Time&lt;/a&gt;, it just downloads corrupt docx files. I am not sure whether this is user error (probably by the document uploader) or a system error, but I fixed it.&lt;/p&gt;&lt;h2&gt;Issue&lt;/h2&gt;&lt;p&gt;When downloading a .docx file from Decision Time as a meeting attendee, the file is corrupt and cannot be opened (or opens with warnings). The document can still be viewed in the web interface. This means that people who rely on a screen reader cannot view the file with their preferred software, and also prevents sharing the documents with others.&lt;/p&gt;&lt;p&gt;Other document types such as PDF are also affected but PDF viewers ignore the leading newline, so it does not actually show an error.&lt;/p&gt;&lt;h2&gt;Solution&lt;/h2&gt;&lt;p&gt;I investigated the file and found that a newline character is erroneously inserted at the start of the file. I have created a &lt;a href=&quot;http://hack5.dev/files/decisiontime_fix-1.0.xpi&quot;&gt;Firefox extension to fix it&lt;/a&gt; by automatically removing the extra newline when you download the file (if you downloaded a corrupt copy before, you will need to download it again before it works).&lt;/p&gt;&lt;p&gt;If you do not want to use the extension, follow these steps:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Download the file.&lt;/li&gt;&lt;li&gt;Rename it to end in &lt;code&gt;.txt&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;Open it in a plain text editor such as Notepad.&lt;/li&gt;&lt;li&gt;The file will start with a blank line, then the letters &lt;code&gt;PK&lt;/code&gt;, then lots of nonsense characters. Remove the blank line so that the file starts immediately with &lt;code&gt;PK&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;Save your changes and rename the file to end with &lt;code&gt;.docx&lt;/code&gt; again.&lt;/li&gt;&lt;/ol&gt;</description>
        <link>https://hack5.dev/blog/decisiontime-fix/</link>
        <pubDate>Sat, 09 May 2026 14:00:00 +0000</pubDate>
        <guid>https://hack5.dev/blog/decisiontime-fix/</guid>
      </item>
    
      <item>
        <title>Jupyter as Markdown for screen readers</title>
        <description>&lt;h2&gt;Abstract&lt;/h2&gt;&lt;p&gt;You can edit a Jupyter notebook as markdown without ever leaving text mode. This can work around accessibility issues or be used on non-graphical machines.&lt;/p&gt;&lt;h2&gt;Introduction&lt;/h2&gt;&lt;p&gt;Trying to use Orca with Jupyter is nigh on impossible. It doesn’t even let you open a file without using the mouse, and mouse routing buttons don’t seem to work for me. The same applies to JupyterLab, and I’ve heard similar stories for them under JAWS.&lt;/p&gt;&lt;p&gt;A lot of great work was started to improve this&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-1&quot; id=&quot;fn-1-ref-1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;, but at the moment I wasn’t able to make any headway using it, and I couldn’t find any sign of ongoing work with a quick search.&lt;/p&gt;&lt;p&gt;Using VSCodium may be an alternative but didn’t work for me. I heard it was heavy and slow to navigate, but your mileage may vary.&lt;/p&gt;&lt;p&gt;I wanted to find a way to use and author Jupyter notebooks using plain text, to make it accessible to screen reader users. This is likely to be less accessible to many people, but by introducing more options, more people will find something that suits them.&lt;/p&gt;&lt;p&gt;I found a lot of tooling for Jupyter&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-2&quot; id=&quot;fn-2-ref-1&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; which led to the discovery of &lt;a href=&quot;https://github.com/takluyver/bookbook&quot;&gt;Bookbook&lt;/a&gt;, a tool that sounded cool but one-way and unmaintained. &lt;a href=&quot;https://github.com/mwouts/jupytext&quot;&gt;JupyText&lt;/a&gt; is a very cool two-way converter, but it doesn’t seem to include cell outputs by default so would preclude easy access to results. I also found &lt;a href=&quot;https://github.com/aaren/notedown/&quot;&gt;Notedown&lt;/a&gt; and its de facto successor &lt;a href=&quot;https://nbconvert.readthedocs.io/en/latest/&quot;&gt;nbconvert&lt;/a&gt;.&lt;/p&gt;&lt;h2&gt;Solution&lt;/h2&gt;&lt;p&gt;This covers the end-to-end workflow of downloading and modifying a notebook from a JupyterLab instance, using nbconvert and pandoc.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Install dependencies&lt;/li&gt;&lt;li&gt;Download all the files from the JupyterLab server&lt;/li&gt;&lt;li&gt;Convert notebook to markdown&lt;/li&gt;&lt;li&gt;Edit and convert back again to execute&lt;/li&gt;&lt;li&gt;Iterate as usual&lt;/li&gt;&lt;/ol&gt;&lt;h3&gt;Dependency installation&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://nbconvert.readthedocs.io/en/latest/install.html&quot;&gt;nbconvert&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://pandoc.org/&quot;&gt;Pandoc&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;Downloading notebooks&lt;/h3&gt;&lt;p&gt;Update: I have written some JavaScript that automatically downloads a zip file from a JupyterLab instance. To use it, open the JupyterLab instance and press F12 to open developer tools. Press Escape to open the Console, then paste the code. After about 2 minutes, it will download a zip file which contains the entire notebook so that you can use it locally in your preferred software. I have only tested this in Firefox so your mileage may vary. If it doesn’t work, try the old annoying way below.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;evaluate&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;apos;//*[@class=&amp;quot;jp-LauncherCard&amp;quot;][@title=&amp;quot;Python (Pyodide)&amp;quot;][@data-category=&amp;quot;Console&amp;quot;]&amp;apos;&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant_builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;XPathResult&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property&quot;&gt;ANY_UNORDERED_NODE_TYPE&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant_builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property&quot;&gt;singleNodeValue&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;created console&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;d&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;DataTransfer&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;quot;Jupyter &amp;quot;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;toISOString&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;.zip&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;apos;import zipfile, os\n&amp;apos;&lt;/span&gt;                      &lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt;
             &lt;span class=&quot;string&quot;&gt;&amp;apos;zf = zipfile.ZipFile(&amp;quot;&amp;apos;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;apos;&amp;quot;, &amp;quot;w&amp;quot;)\n&amp;apos;&lt;/span&gt;   &lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt;
             &lt;span class=&quot;string&quot;&gt;&amp;apos;for dp, _, fns in os.walk(&amp;quot;.&amp;quot;):\n&amp;apos;&lt;/span&gt;        &lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt;
             &lt;span class=&quot;string&quot;&gt;&amp;apos;    for fn in fns:\n&amp;apos;&lt;/span&gt;                     &lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt;
             &lt;span class=&quot;string&quot;&gt;&amp;apos;        if fn == &amp;quot;&amp;apos;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;apos;&amp;quot;:\n&amp;apos;&lt;/span&gt;            &lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt;
             &lt;span class=&quot;string&quot;&gt;&amp;apos;            continue\n&amp;apos;&lt;/span&gt;                   &lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt;
             &lt;span class=&quot;string&quot;&gt;&amp;apos;        zf.write(os.path.join(dp, fn))\n&amp;apos;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt;
             &lt;span class=&quot;string&quot;&gt;&amp;apos;zf.close()&amp;apos;&lt;/span&gt;                               &lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;setData&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;text/plain&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable function&quot;&gt;setInterval&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property&quot;&gt;activeElement&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property&quot;&gt;classList&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;contains&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;cm-content&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property&quot;&gt;activeElement&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;dispatchEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;ClipboardEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;paste&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;clipboardData&lt;/span&gt;: &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;dataType&lt;/span&gt;: &lt;span class=&quot;string&quot;&gt;&amp;quot;text/plain&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;data&lt;/span&gt;: &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;pasted script&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;getElementsByClassName&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;jp-CodeConsole-promptCell&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;getElementsByClassName&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;cm-content&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;dispatchEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;FocusEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;focus&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;constructor constant variable_builtin function_builtin variable function&quot;&gt;clearInterval&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;focused input&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable function&quot;&gt;setInterval&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;e&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;evaluate&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;apos;//*[@data-command=&amp;quot;console:run-forced&amp;quot;]&amp;apos;&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant_builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;XPathResult&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property&quot;&gt;ANY_UNORDERED_NODE_TYPE&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant_builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property&quot;&gt;singleNodeValue&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;e&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;constant_builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;disabled&amp;quot;&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property&quot;&gt;attributes&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;getElementsByClassName&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;jp-CodeConsole-promptCell&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;getElementsByClassName&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;cm-content&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property&quot;&gt;innerText&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;dispatchEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;MouseEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;mousedown&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;bubbles&lt;/span&gt;: &lt;span class=&quot;constant_builtin&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;cancelable&lt;/span&gt;: &lt;span class=&quot;constant_builtin&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;view&lt;/span&gt;: &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;window&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;dispatchEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;MouseEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;mouseup&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;bubbles&lt;/span&gt;: &lt;span class=&quot;constant_builtin&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;cancelable&lt;/span&gt;: &lt;span class=&quot;constant_builtin&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;view&lt;/span&gt;: &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;window&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;constructor constant variable_builtin function_builtin variable function&quot;&gt;clearInterval&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;clicked run&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;k&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable function&quot;&gt;setInterval&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;evaluate&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;apos;//*[contains(concat(&amp;quot; &amp;quot;,normalize-space(@class),&amp;quot; &amp;quot;),&amp;quot; jp-InputPrompt &amp;quot;)][text()=&amp;quot;[*]:&amp;quot;]&amp;apos;&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant_builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;XPathResult&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property&quot;&gt;ANY_UNORDERED_NODE_TYPE&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant_builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property&quot;&gt;singleNodeValue&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;constant_builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;waiting for execution&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;e&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;evaluate&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;apos;//*[@class=&amp;quot;jp-DirListing-itemText&amp;quot;]//*[text()=&amp;quot;&amp;apos;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;apos;&amp;quot;]&amp;apos;&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant_builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;XPathResult&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property&quot;&gt;ANY_UNORDERED_NODE_TYPE&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant_builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property&quot;&gt;singleNodeValue&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;e&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;constant_builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;e2&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;closest&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;.jp-DirListing-item&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;e2&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;dispatchEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;PointerEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;mousedown&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;bubbles&lt;/span&gt;: &lt;span class=&quot;constant_builtin&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;cancelable&lt;/span&gt;: &lt;span class=&quot;constant_builtin&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;e2&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;dispatchEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;PointerEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;mouseup&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;bubbles&lt;/span&gt;: &lt;span class=&quot;constant_builtin&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;cancelable&lt;/span&gt;: &lt;span class=&quot;constant_builtin&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;dispatchEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;PointerEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;contextmenu&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;bubbles&lt;/span&gt;: &lt;span class=&quot;constant_builtin&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;cancelable&lt;/span&gt;: &lt;span class=&quot;constant_builtin&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;constructor constant variable_builtin function_builtin variable function&quot;&gt;clearInterval&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;right-clicked file&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;l&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable function&quot;&gt;setInterval&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;e&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;evaluate&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;apos;//*[@data-command=&amp;quot;filebrowser:download&amp;quot;]&amp;apos;&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant_builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;XPathResult&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property&quot;&gt;ANY_UNORDERED_NODE_TYPE&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant_builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property&quot;&gt;singleNodeValue&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;e&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;constant_builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;dispatchEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;FocusEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;focus&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;focused download button&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;dispatchEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;PointerEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;mousedown&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;bubbles&lt;/span&gt;: &lt;span class=&quot;constant_builtin&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;cancelable&lt;/span&gt;: &lt;span class=&quot;constant_builtin&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;dispatchEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;PointerEvent&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;mouseup&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;bubbles&lt;/span&gt;: &lt;span class=&quot;constant_builtin&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;cancelable&lt;/span&gt;: &lt;span class=&quot;constant_builtin&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;property function_method&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;quot;clicked download button&amp;quot;&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;constructor constant variable_builtin function_builtin variable function&quot;&gt;clearInterval&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constructor constant variable_builtin function_builtin variable&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;punctuation_bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation_bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation_delimiter&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;details class=&quot;block&quot;&gt;&lt;summary&gt;The old annoying way to download the notebook&lt;/summary&gt;&lt;p&gt;This code converts an online Jupyter notebook into a zip file that you can download. To run it, you need to launch a Python interpreter in the inaccessible web interface and paste this code, then press Shift+Enter. There will probably be no output that a screen reader can understand, but a file named “lab.zip” will appear in the “file explorer” in the web interface, which you can download by right-clicking and choosing Download.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;constructor constant variable&quot;&gt;zipfile&lt;/span&gt;, &lt;span class=&quot;constructor constant variable&quot;&gt;os&lt;/span&gt;
&lt;span class=&quot;constructor constant variable&quot;&gt;zf&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;constructor constant variable&quot;&gt;zipfile&lt;/span&gt;.&lt;span class=&quot;constructor property constant function_method variable&quot;&gt;ZipFile&lt;/span&gt;(&lt;span class=&quot;string&quot;&gt;&amp;quot;lab.zip&amp;quot;&lt;/span&gt;, &lt;span class=&quot;string&quot;&gt;&amp;quot;w&amp;quot;&lt;/span&gt;)
&lt;span class=&quot;keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;constructor constant variable&quot;&gt;dp&lt;/span&gt;, &lt;span class=&quot;constructor constant variable&quot;&gt;_&lt;/span&gt;, &lt;span class=&quot;constructor constant variable&quot;&gt;fns&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;constructor constant variable&quot;&gt;os&lt;/span&gt;.&lt;span class=&quot;constructor property constant function_method variable&quot;&gt;walk&lt;/span&gt;(&lt;span class=&quot;string&quot;&gt;&amp;quot;.&amp;quot;&lt;/span&gt;):
 &lt;span class=&quot;keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;constructor constant variable&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;constructor constant variable&quot;&gt;fns&lt;/span&gt;:
   &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;constructor constant variable&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;quot;lab.zip&amp;quot;&lt;/span&gt;:
     &lt;span class=&quot;keyword&quot;&gt;continue&lt;/span&gt;
   &lt;span class=&quot;constructor constant variable&quot;&gt;zf&lt;/span&gt;.&lt;span class=&quot;constructor property constant function_method variable&quot;&gt;write&lt;/span&gt;(&lt;span class=&quot;constructor constant variable&quot;&gt;os&lt;/span&gt;.&lt;span class=&quot;constructor property constant variable&quot;&gt;path&lt;/span&gt;.&lt;span class=&quot;constructor property constant function_method variable&quot;&gt;join&lt;/span&gt;(&lt;span class=&quot;constructor constant variable&quot;&gt;dp&lt;/span&gt;, &lt;span class=&quot;constructor constant variable&quot;&gt;fn&lt;/span&gt;))
&lt;span class=&quot;constructor constant variable&quot;&gt;zf&lt;/span&gt;.&lt;span class=&quot;constructor property constant function_method variable&quot;&gt;close&lt;/span&gt;()
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;&lt;hr&gt;&lt;p&gt;Now decompress the files by double-clicking them (or using a terminal as below).&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;constant function&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;lab&lt;/span&gt;
&lt;span class=&quot;constant function&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;lab&lt;/span&gt;
&lt;span class=&quot;constant function&quot;&gt;unzip&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;../lab.zip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that if you don’t entirely trust the files, you’ll need to remove the &lt;code&gt;--execute&lt;/code&gt; in the next section. If you really don’t trust them at all, don’t download them!&lt;/p&gt;&lt;h3&gt;Converting to markdown&lt;/h3&gt;&lt;p&gt;The magic command is:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;constant function&quot;&gt;jupyter&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;nbconvert&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;--execute&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;--allow-errors&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;--inplace&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;notebook.ipynb&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;constant function&quot;&gt;pandoc&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;--extract-media=images&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;notebook.ipynb&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;notebook.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will run the code (&lt;code&gt;--execute&lt;/code&gt;) and save the readable file into notebook.md which you can edit using your favourite text editor. Images such as graphs will be saved into the images directory.&lt;/p&gt;&lt;p&gt;If you don’t entirely trust the code, remove the &lt;code&gt;--execute&lt;/code&gt; flag (technically you can skip the entire first part of the pipeline but that’s harder to explain)&lt;/p&gt;&lt;p&gt;If you run into an error about NoSuchKernel&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-3&quot; id=&quot;fn-3-ref-1&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;, try the following command, which will tell it to use the default kernel. This will fix the error but may change behaviour. You will still need to run the conversion command again afterwards.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;constant function&quot;&gt;jq&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;string constant&quot;&gt;&amp;apos;.metadata.kernelspec.name = &amp;quot;python&amp;quot;&amp;apos;&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;notebook.ipynb&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;gt;&lt;/span&gt; notebook.ipynb.tmp &lt;span class=&quot;operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;constant function&quot;&gt;mv&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;notebook.ipynb.tmp&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;notebook.ipynb&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Converting back to Jupyter format&lt;/h3&gt;&lt;p&gt;To test your code, first turn it back into a Jupyter notebook:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;constant function&quot;&gt;pandoc&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;notebook.md&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;notebook.ipynb&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then just turn it back into Markdown like before, and any errors will show up below the corresponding code (you can search &lt;code&gt;.output .error&lt;/code&gt; to find them). You could also remove the &lt;code&gt;--allow-errors&lt;/code&gt; flag to have the errors show up without updating the Markdown file, but I think this would be more confusing.&lt;/p&gt;&lt;h2&gt;Limitations&lt;/h2&gt;&lt;p&gt;The Jupyter web interfaces allow re-running individual blocks of code. I haven’t looked into this for nbconvert, but this feature can accelerate development, so finding a way to add it would be useful. One can imagine that only cells that have changed since the last run would be evaluated by default by a tool incorporating this feature.&lt;/p&gt;&lt;p&gt;The process described is quite clunky and requires considerable technical knowledge. Markdown is a fairly easy language to learn, so with the advent of less expensive multi-line Braille displays&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-4&quot; id=&quot;fn-4-ref-1&quot;&gt;4&lt;/a&gt;&lt;/sup&gt; there is scope for a simple application that ties the whole workflow together into a simple editor with sensible keyboard shortcuts and search functionality.&lt;/p&gt;&lt;h2&gt;Conclusion&lt;/h2&gt;&lt;p&gt;This workflow can help with Jupyter usage for some people whose accessibility needs are not met by the existing solutions, but it’s not perfect. People who just love their machines in text-mode may prefer &lt;a href=&quot;https://github.com/kracekumar/jut&quot;&gt;jut&lt;/a&gt;. People who want to commit plain-text versions of Jupyter files may fare better with &lt;a href=&quot;https://github.com/mwouts/jupytext&quot;&gt;Jupytext&lt;/a&gt;.&lt;/p&gt;&lt;h2&gt;P.S.&lt;/h2&gt;&lt;p&gt;Wondering why the writing style is so weird? Reading too many academic papers while trying to write casually at one in the morning don’t mix well.&lt;/p&gt;&lt;p&gt;All code in this post is licensed under &lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;CC BY 4.0&lt;/a&gt;&lt;img src=&quot;https://mirrors.creativecommons.org/presskit/icons/cc.svg&quot; alt=&quot;&quot; style=&quot;max-width: 1em;max-height:1em;margin-left: .2em;&quot;&gt;&lt;img src=&quot;https://mirrors.creativecommons.org/presskit/icons/by.svg&quot; alt=&quot;&quot; style=&quot;max-width: 1em;max-height:1em;margin-left: .2em;&quot;&gt;&lt;/p&gt;
</description>
        <link>https://hack5.dev/blog/accessible-jupyter/</link>
        <pubDate>Thu, 19 Feb 2026 00:40:00 +0000</pubDate>
        <guid>https://hack5.dev/blog/accessible-jupyter/</guid>
      </item>
    
      <item>
        <title>GnuPG in Thunderbird in Flatpak in Silverblue</title>
        <description>&lt;p&gt;Thunderbird’s already flaky GPG smartcard support falls down when using Flatpak. But fear not - I managed to get it working!&lt;/p&gt;&lt;h2&gt;Summary&lt;/h2&gt;&lt;p&gt;To get your Yubikey working on Fedora Silverblue with Thunderbird, follow these steps:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;Fix hotplug&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-1&quot; id=&quot;fn-1-ref-1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;$ cat &amp;gt;&amp;gt; ~/.gnupg/scdaemon.conf &amp;lt;&amp;lt;EOF
disable-ccid
pcsc-shared
EOF
$
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Enable &lt;code&gt;gpg-agent.socket&lt;/code&gt;&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-2&quot; id=&quot;fn-2-ref-1&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;systemctl --user enable --now gpg-agent.socket
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Grant Thunderbird access to the system gpg-agent instead of running its own agent:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;flatpak override --user --socket=gpg-agent --nosocket=pcsc &amp;apos;--nofilesystem=~/.gnupg&amp;apos; net.thunderbird.Thunderbird
flatpak override --user &amp;apos;--filesystem=~/.gnupg:ro&amp;apos;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Fix the session DBus&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-3&quot; id=&quot;fn-3-ref-1&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-4&quot; id=&quot;fn-4-ref-1&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;sudo tee /opt/pinentry.sh &amp;lt;&amp;lt;EOF
#!/bin/env -S --ignore-environment /bin/sh

# The shebang using env ensures that all (potentially malicious) environment variables are cleared

# Set the correct DBus session bus path - this is hardcoded because we don&amp;apos;t trust the environment we receive if called from a sandboxed caller
export DBUS_SESSION_BUS_ADDRESS=&amp;quot;unix:path=/run/user/$(/usr/bin/id -ru)/bus&amp;quot;
# Set a placeholder display, otherwise the curses fallback is sometimes used.
# In particular this makes a difference if the caller doesn&amp;apos;t specify --display.
# This might break if you use X11, but if you are, stop, and use Wayland instead.
export DISPLAY=&amp;quot;__stub&amp;quot;

# And finally pass everything else through
exec /usr/bin/pinentry &amp;quot;$@&amp;quot;
EOF
sudo chown root:root /opt/pinentry.sh
sudo chmod 0755 /opt/pinentry.sh
sudo chcon -t pinentry_exec_t /opt/pinentry.sh
echo &amp;quot;pinentry-program /opt/pinentry.sh&amp;quot; &amp;gt;&amp;gt; ~/.gnupg/gpg-agent.conf
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Reboot (I mean it!)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Follow the &lt;a href=&quot;https://wiki.mozilla.org/Thunderbird:OpenPGP:Smartcards&quot;&gt;normal setup instructions for Thunderbird&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Other useful steps you might want to take are found in the &lt;a href=&quot;https://github.com/drduh/YubiKey-Guide&quot;&gt;community guide&lt;/a&gt;.&lt;/p&gt;&lt;h2&gt;Thunderbird debugging&lt;/h2&gt;&lt;p&gt;I started off looking at the Thunderbird error log (Ctrl+Shift+J), but it was rather unhelpful:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;mailnews.send: NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS: [JavaScript Error: &amp;quot;encryptMessageStart FAILED: -1&amp;quot; {file: &amp;quot;chrome://openpgp/content/modules/mimeEncrypt.sys.mjs&amp;quot; line: 455}]&amp;apos;[JavaScript Error: &amp;quot;encryptMessageStart FAILED: -1&amp;quot; {file: &amp;quot;chrome://openpgp/content/modules/mimeEncrypt.sys.mjs&amp;quot; line: 455}]&amp;apos; when calling method: [nsIMsgComposeSecure::finishCryptoEncapsulation]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Annoyingly that code was quite tricky to trace but I eventually found that it &lt;a href=&quot;https://searchfox.org/comm-central/rev/f32ac8259c429ae1655e068a3f6bf65df5ee4361/mail/extensions/openpgp/content/modules/RNP.sys.mjs#3783&quot;&gt;called into a library called GPGME&lt;/a&gt;.&lt;/p&gt;&lt;h2&gt;GPGME debugging&lt;/h2&gt;&lt;p&gt;Thankfully, there is pdocumentation on how to &lt;a href=&quot;https://www.gnupg.org/documentation/manuals/gpgme/Debugging.html&quot;&gt;debug GPGME&lt;/a&gt;!&lt;/p&gt;&lt;pre&gt;&lt;code&gt;flatpak run --env=GPGME_DEBUG=9 net.thunderbird.Thunderbird
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And suddenly I was able to see the actual error while trying to send signed mail!&lt;/p&gt;&lt;pre&gt;&lt;code&gt;2025-08-13 11:05:34 gpgme[2.2]     gpgme_op_keylist_next:1367: error: End of file &amp;lt;GPGME&amp;gt;\n
2025-08-13 11:05:34 gpgme[2.2]     gpgme_release: call: ctx=0x00007f633fdfeac0 
2025-08-13 11:05:34 gpgme[2.2]     gpgme_data_release: call: dh=0x0000000000000000 
2025-08-13 11:05:34 gpgme[2.2]     gpgme_data_release: call: dh=0x00007f633e165000 
2025-08-13 11:05:34 gpgme[2.2]   gpgme_get_key:1495: error: End of file &amp;lt;GPGME&amp;gt;\n
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Okay, not a super helpful error. But scrolling up a bit (a lot, these logs are super verbose), I found something more useful:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;2025-08-13 11:05:34 gpgme[2.2]         _gpgme_io_read: check: gpg: error running &amp;apos;/usr/bin/gpg-agent&amp;apos;: exit st
2025-08-13 11:05:34 gpgme[2.2]         _gpgme_io_read: check: atus 2&amp;lt;LF&amp;gt;
2025-08-13 11:05:34 gpgme[2.2]         _gpgme_io_read: check: gpg: failed to start gpg-agent &amp;apos;/usr/bin/gpg-age
2025-08-13 11:05:34 gpgme[2.2]         _gpgme_io_read: check: nt&amp;apos;: General error&amp;lt;LF&amp;gt;
2025-08-13 11:05:34 gpgme[2.2]         _gpgme_io_read: check: gpg: can&amp;apos;t connect to the gpg-agent: General err
2025-08-13 11:05:34 gpgme[2.2]         _gpgme_io_read: check: or&amp;lt;LF&amp;gt;
2025-08-13 11:05:34 gpgme[2.2]         _gpgme_io_read: check: gpg: keydb_search failed: No agent running&amp;lt;LF&amp;gt;
2025-08-13 11:05:34 gpgme[2.2]         _gpgme_io_read: check: gpg: error reading key: No agent running&amp;lt;LF&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;The GPG agent&lt;/h2&gt;&lt;p&gt;So it looks like we’re struggling to connect to the GPG agent. Looking at the pid of the GPG agent from outside the flatpak sandbox, I was able to confirm this:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;$ pidof gpg-agent
$ 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Initially I decided to postpone fixing this issue and just manually start a GPG agent to see if we could make any other progress, so I just started an agent manually by calling a GPG command on the host.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;$ gpg --card-status
&amp;lt;snip&amp;gt;
$ pidof gpg-agent
24067
$ 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Great, let’s try sending another email.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;2025-08-13 11:08:52 gpgme[2.2]   gpgme_op_sign:522: error: pinentry error &amp;lt;Pinentry&amp;gt;\n
&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Pinentry inside the container&lt;/h2&gt;&lt;p&gt;A new error! My first instinct here was that the sandbox was blocking us from requesting the PIN, so I decided to test it out inside the Flatpak:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;$ flatpak enter net.thunderbird.Thunderbird bash
bash-5.2$ pinentry
OK Pleased to meet you
GETPIN
&amp;lt;snip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Unfortunately, this opened a Curses interface.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;bash-5.2$ pinentry --help
pinentry-curses (pinentry) 1.3.1-unknown
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Wrong pinentry… but there’s a &lt;code&gt;pinentry-gnome3&lt;/code&gt; binary installed in the Flatpak.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;bash-5.2$ pinentry-gnome3 --debug
No Gcr System Prompter available, falling back to curses
OK Pleased to meet you
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It complained that there was something wrong with the connection to &lt;a href=&quot;https://packages.fedoraproject.org/pkgs/gcr/gcr/&quot;&gt;gcr&lt;/a&gt;, but wouldn’t say what.&lt;/p&gt;&lt;p&gt;At this point I went down a rabbithole and compiled my own version of &lt;code&gt;pinentry-gnome3&lt;/code&gt; with the fallback disabled, which showed me the actual error message. Basically it was complaining that a session-bus DBus call was failing (although it didn’t say what).&lt;/p&gt;&lt;h2&gt;DBus allowlisting&lt;/h2&gt;&lt;p&gt;Luckily, Flatpak has &lt;a href=&quot;https://docs.flatpak.org/en/latest/debugging.html#audit-session-or-system-bus-traffic&quot;&gt;documentation on monitoring DBus traffic&lt;/a&gt;. Following this, I saw the following:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;Filtering message due to arg0 org.gnome.keyring.SystemPrompter, policy: 0 (required 1)
C4: -&amp;gt; org.gnome.keyring.SystemPrompter call org.gnome.keyring.internal.Prompter.BeginPrompting at /org/gnome/keyring/Prompter
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;At this point, I tried granting &lt;code&gt;org.gnome.keyring.*&lt;/code&gt; to the Flatpak to see if it would fix pinentry, and it did!&lt;/p&gt;&lt;pre&gt;&lt;code&gt;bash-5.2$ pinentry-gnome3 --debug
OK Pleased to meet you
GETPIN
D testing
OK
^C
bash-5.2$
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This got me excited so I tried sending an email, but I still got the &lt;em&gt;exact&lt;/em&gt; same error:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;gpgme_op_sign:522: error: pinentry error &amp;lt;Pinentry&amp;gt;\n
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This was most discouraging as it suggested I had fixed the wrong problem, especially as there was nothing in the DBus logs.&lt;/p&gt;&lt;h2&gt;Pinentry logging&lt;/h2&gt;&lt;p&gt;I decided to take a new approach and try calling gpg manually inside the flatpak container. To my surprise, this worked! And what’s more, after manually signing a message (which correctly prompted for my PIN), I was able to send emails through Thunderbird!&lt;/p&gt;&lt;p&gt;This was definitely progress but after restarting Thunderbird it was broken again. I decided to try to get access to the actual error returned by pinentry in real operation.&lt;/p&gt;&lt;p&gt;I created a bash wrapper script:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;#!/bin/sh
echo &amp;quot;pinentry called with:&amp;quot; &amp;quot;$@&amp;quot; &amp;gt;&amp;gt; ~/pinentry.log
tee -a ~/pinentry.in.log | pinentry --debug 2&amp;gt;&amp;gt;~/pinentry.err.log | tee -a ~/pinentry.out.log
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I configured it with &lt;code&gt;pinentry-program&lt;/code&gt; in &lt;code&gt;gpg-agent.conf&lt;/code&gt;. At this point after failing to send an email I had full logs from the pinentry.&lt;/p&gt;&lt;p&gt;Most interesting was the stderr output:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;failed to connect to user session D-Bus (1): Could not connect: No such file or directory
Timeout: the Gcr system prompter was already in use.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This initially surprised me as I had been able to connect to the session bus in my manual tests of pinentry. In order to try to reproduce the issue, I decided to have my script print the entire &lt;code&gt;env&lt;/code&gt; and &lt;code&gt;/proc/$$/status&lt;/code&gt; into my log files.&lt;/p&gt;&lt;p&gt;I noticed that the &lt;code&gt;PPid&lt;/code&gt; was one of two gpg-agent processes running on my system!&lt;/p&gt;&lt;p&gt;At this point, I wanted to ensure that there was only one gpg-agent, and I wanted it outside the sandbox so that I could use it from the console. A quick search online told me that it was &lt;a href=&quot;https://superuser.com/a/1608985&quot;&gt;a one-liner&lt;/a&gt;!&lt;/p&gt;&lt;p&gt;I did this and rebooted. I also noticed in Flatseal that I could enable gpg-agent socket sharing but that this was currently disabled. I enabled this and disabled the pcsc socket to avoid conflicts.&lt;/p&gt;&lt;p&gt;At this point I was still encountering the same error but at least I knew that the &lt;a href=&quot;https://dev.gnupg.org/T6058&quot;&gt;pinentry was meant to be running outside the sandbox&lt;/a&gt;, and the PPid confirmed that. However, the same error was still occurring.&lt;/p&gt;&lt;p&gt;I noticed in the logs that there was a DBus environment variable set:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;DBUS_SESSION_BUS_ADDRESS=unix:path=/run/flatpak/bus
&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Containment confusion&lt;/h2&gt;&lt;p&gt;This was most odd: we know that pinentry is not running in Flatpak, so it definitely can’t access a Flatpak DBus proxy.&lt;/p&gt;&lt;p&gt;At this point, I was pretty sure I had my culprit. After some extensive searching I found that gpg agent clients are allowed to send certain environment variables to pinentry using &lt;a href=&quot;https://www.gnupg.org/documentation/manuals/gnupg/Agent-OPTION.html&quot;&gt;OPTION putenv&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Sadly, there doesn’t seem to be a way to disable &lt;code&gt;putenv&lt;/code&gt;. It sounds like a sandbox escape waiting to happen. To test my hypothesis, I grabbed my unsandboxed DBus address:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;$ echo $DBUS_SESSION_BUS_ADDRESS
unix:path=/run/user/1000/bus
$ 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;and I entered this manually into my &lt;code&gt;pinentry.sh&lt;/code&gt; wrapper. Lo and behold, I was able to send signed emails!&lt;/p&gt;&lt;p&gt;However, now that I knew about the &lt;code&gt;putenv&lt;/code&gt; option, I realised that the wrapper would need some additional security measures to avoid vulnerabilities. For example, I wanted to use &lt;code&gt;/run/user/$(id -ru)/bus&lt;/code&gt; as the session bus, but calling &lt;code&gt;id&lt;/code&gt; could be subject to &lt;a href=&quot;https://attack.mitre.org/techniques/T1574/007/&quot;&gt;PATH Interception&lt;/a&gt;. To avoid this, I decided to use &lt;code&gt;env -i&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;#!/bin/env -S --ignore-environment /bin/sh
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;which invokes the shell script with an entirely clean environment.&lt;/p&gt;&lt;p&gt;Update: It turns out that &lt;code&gt;putenv&lt;/code&gt; only lets you edit a restricted set of environment variables. Nevertheless, clearing out the environment if we don’t need it is a good idea for Defence in Depth.&lt;/p&gt;&lt;p&gt;Finally I dumped the script in &lt;code&gt;/opt&lt;/code&gt; and configured it.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;#!/bin/env -S --ignore-environment /bin/sh

# The shebang using env ensures that all (potentially malicious) environment variables are cleared

# Set the correct DBus session bus path - this is hardcoded because we don&amp;apos;t trust the environment we receive if called from a sandboxed caller
export DBUS_SESSION_BUS_ADDRESS=&amp;quot;unix:path=/run/user/$(/usr/bin/id -ru)/bus&amp;quot;

# And finally pass everything else through
exec /usr/bin/pinentry &amp;quot;$@&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;pinentry-program /opt/pinentry.sh
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;At this point, I had working emails. After some testing, it seems that this also fixed decryption for free.&lt;/p&gt;&lt;p&gt;This broke some other programs - turns out that the curses fallback is activated if &lt;code&gt;DISPLAY&lt;/code&gt; is unset and &lt;code&gt;--display&lt;/code&gt; is not passed. Setting &lt;code&gt;DISPLAY=__stub&lt;/code&gt; fixed this.&lt;/p&gt;&lt;h2&gt;Alternatives&lt;/h2&gt;&lt;p&gt;I decided to run the GPG agent outside the sandbox. In theory, it could run inside the Flatpak (and that seems to be the design intended by the Thunderbird team), but this would come with some tradeoffs:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;This means that &lt;code&gt;pinentry&lt;/code&gt; would run inside the sandbox which means a compromised container could read your smartcard PIN, potentially allowing other credentials to be compromised.&lt;/li&gt;&lt;li&gt;On the other hand, running &lt;code&gt;pinentry&lt;/code&gt; outside the sandbox increases the attack surface for a sandbox escape. This is arguably mitigated by removing access to pcscd which is no longer required.&lt;/li&gt;&lt;li&gt;Most importantly, running &lt;code&gt;gpg-agent&lt;/code&gt; outside the sandbox means that &lt;code&gt;~/.gnupg&lt;/code&gt; can be exposed to the sandbox in read-only mode. When this is read-write, sandbox escape is trivial as it can just edit the configuration file to make the system-wide GPG agent invoke a malicious pinentry program.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I also wasn’t able to get the agent working reliably inside the sandbox, which is the main reason for running it outside.&lt;/p&gt;&lt;h2&gt;Conclusion&lt;/h2&gt;&lt;p&gt;At the start of this process I knew very little about GPG’s internal workings and now I feel like I do at least understand some of the PIN entry architecture and the purpose of the agent.&lt;/p&gt;&lt;p&gt;It also reminded me of some of my Flatpak knowledge that was getting rusty. I should probably have noticed faster that the &lt;code&gt;pinentry&lt;/code&gt; call was in the wrong environment when it didn’t show up in the DBus logs during email sending attempts.&lt;/p&gt;</description>
        <link>https://hack5.dev/blog/thunderbird-yubikey-silverblue/</link>
        <pubDate>Wed, 13 Aug 2025 11:29:00 +0000</pubDate>
        <guid>https://hack5.dev/blog/thunderbird-yubikey-silverblue/</guid>
      </item>
    
      <item>
        <title>Thoughts on Golang</title>
        <description>&lt;p&gt;Over the last year, I’ve had the opportunity to work a lot with Golang code. It has certainly been an interesting experience, as I’ve never seriously used Go before.&lt;/p&gt;&lt;h2&gt;Quick overview&lt;/h2&gt;&lt;p&gt;Golang is a programming language created by Google. It’s fairly low-level and compiles to machine code, but is designed for general-purpose use and has automatic memory management.&lt;/p&gt;&lt;h2&gt;Paradigm&lt;/h2&gt;&lt;p&gt;Golang &lt;em&gt;feels&lt;/em&gt; like a piece of software designed with system-driven design rather than user-centred design. The stated design goal is to keep the language simple. This has both positive and negative results.&lt;/p&gt;&lt;p&gt;Firstly, I disagree with the implied statement that a simple language is an imperative language. It’s possible to write a language that is no more complex than Golang which supports, at the very least, easy use of functional patterns mixed in with imperative code.&lt;/p&gt;&lt;h2&gt;Tooling&lt;/h2&gt;&lt;h3&gt;The package manager&lt;/h3&gt;&lt;p&gt;&lt;code&gt;go.mod&lt;/code&gt; is great. For those who are unfamiliar with it, Golang will simply refuse to link any packages into your code unless they are listed in the &lt;code&gt;go.mod&lt;/code&gt; residing in the root of your codebase. More languages should do this. Why? It prevents you accidentally introducting unwanted dependencies into your project.&lt;/p&gt;&lt;p&gt;&lt;code&gt;go.sum&lt;/code&gt;, meanwhile. I’m not a big fan of. It lists the checksums of all packages in the &lt;code&gt;go.mod&lt;/code&gt; file. This is great for security reasons as it provides Trust On First Use security for all your dependencies. This means that if an upstream source becomes compromised and retags a version tag to some malicious code, you’re safe. However, it’s quite frustrating to have to run a command to update it every time you modify your dependencies. It feels like this could be done automatically, and is only this way due to system-centred design. Of course you can call &lt;code&gt;go get&lt;/code&gt; but it’s slow with non-obvious syntax for getting a specific version.&lt;/p&gt;&lt;p&gt;Golang also compiles all dependencies locally. This is good although it can feel frustrating when it takes a long time to compile. But it’s worth it to avoid the likes of the &lt;a href=&quot;https://en.wikipedia.org/wiki/XZ_Utils_backdoor&quot;&gt;xz-utils backdoor&lt;/a&gt;. I have no complaints here.&lt;/p&gt;&lt;h3&gt;Containerisation&lt;/h3&gt;&lt;p&gt;There are various approaches to building Golang code into a container.&lt;/p&gt;&lt;p&gt;You can go with the simple approach of making a Dockerfile that calls &lt;code&gt;go build&lt;/code&gt;, but then you end up needing to download the dependencies during every build process where a cache miss occurs. Getting the Dockerfile to build quickly is rather tricky as Docker doesn’t make explicit caching sharing very easy, especially if you build multiple containers from the same source (or with common dependencies).&lt;/p&gt;&lt;p&gt;Since a Golang project contains (by definition, in the &lt;code&gt;go.sum&lt;/code&gt;) a hash of every dependency, the build process is quasi-reproducible&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-1&quot; id=&quot;fn-1-ref-1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. This means that we can safely bypass most of the complicated work that Docker does to protect us from external influences during build.&lt;/p&gt;&lt;p&gt;And that’s exactly what &lt;a href=&quot;https://ko.build/&quot;&gt;Ko&lt;/a&gt; does. You simply declare the arguments for the compiler and it handles turning it into a Docker image, basically by running &lt;code&gt;go build&lt;/code&gt; on the host machine then copying into a container. It’s a very nice tool and speeds up the build process by an order of magnitude (in my case, over 3x speedup).&lt;/p&gt;&lt;h3&gt;Linting&lt;/h3&gt;&lt;p&gt;A wise man once told me “programmers should be lazy and stupid”&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-2&quot; id=&quot;fn-2-ref-1&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;. The logic is that a “lazy” programmer won’t over-complicate things, and a good programmer should write code that “stupid” people can understand. What with agentic LLMs writing code, I suspect that code authors are going to get less lazy and more stupid. Since perhaps the most useful input to an agentic system is the criticism, a good linter is essential. Some of the linters in Golang are fragmented, slow and often wrong, which makes it difficult to use AI for development, but also slows down human work.&lt;/p&gt;&lt;h4&gt;thelper&lt;/h4&gt;&lt;p&gt;A common mistake I’ve seen (in human- and AI-written code) is to use &lt;code&gt;t.Helper()&lt;/code&gt; (from &lt;code&gt;testing.T&lt;/code&gt;) in test helpers. &lt;code&gt;T.Helper&lt;/code&gt; has very precise criteria for correct use&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-3&quot; id=&quot;fn-3-ref-1&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;, but the &lt;code&gt;thelper&lt;/code&gt; lint rule complains every time any function accepts a &lt;code&gt;testing.T&lt;/code&gt; parameter. This leads people to think they are making a mistake by not including &lt;code&gt;t.Helper()&lt;/code&gt; at the start of helper functions.&lt;/p&gt;&lt;h3&gt;Test discovery&lt;/h3&gt;&lt;p&gt;A common practice in Golang is to have dynamically-declared subtests (table-based testing). That’s a good thing in most languages, because it avoids code duplication. However, in Golang, the test names aren’t known until after you have started running them. In Java, for example, table-driven tests are brought up at class initialisation time which means they are discoverable. This means that Go subtests are a pain to discover&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-4&quot; id=&quot;fn-4-ref-1&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&lt;h2&gt;Memory management&lt;/h2&gt;&lt;p&gt;Go uses a garbage collector for memory management, and as GCs go, it’s pretty good. It’s not very stop-the-world and can be tuned.&lt;/p&gt;&lt;h3&gt;Race detector&lt;/h3&gt;&lt;p&gt;The race detector is very helpful when testing. It’s very nice to get a warning when your code has a data race, and can save a lot of time debugging in production.&lt;/p&gt;&lt;h2&gt;Code patterns&lt;/h2&gt;&lt;p&gt;Golang’s language design brings about a number of code patterns that I dislike. Some of these are avoidable (and should be avoided!), but others we’re stuck with.&lt;/p&gt;&lt;h3&gt;Slices&lt;/h3&gt;&lt;p&gt;Golang has arrays (fixed length) and slices (variable length) which is slightly more complex than necessary but not &lt;em&gt;bad&lt;/em&gt; (although I’ve seen it cause problems for new developers).&lt;/p&gt;&lt;p&gt;In the interests of keeping the language simple, Go has sacrificed the ability to cast slices. That’s quite good because it means that every piece of syntax is constant time. Except when casting between &lt;code&gt;string&lt;/code&gt; and &lt;code&gt;[]byte&lt;/code&gt;&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-5&quot; id=&quot;fn-5-ref-1&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;. Go has a very frustrating habit of making sensible choices and then giving them weird exceptions for convenience.&lt;/p&gt;&lt;p&gt;Another thing that annoys me is that a slice exposes its capacity all over the place. Surely the underlying capacity of a slice should be an implementation detail? If you need to ensure there exists an Nth element of a slice, you should request a specific length. If you want to request a specific capacity, fine. But you shouldn’t have to consider the capacity of the slice when slicing it. It’s simpler for Go but much more complex for everyone who uses Go. Another example of system-centric design.&lt;/p&gt;&lt;h3&gt;Goroutines&lt;/h3&gt;&lt;p&gt;I used to think goroutines were the &lt;a href=&quot;https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/&quot;&gt;scourge of the entire language&lt;/a&gt;. Now, I’m not so sure. I still think that structured concurrency is the way to go (no pun intended), but when used correctly, goroutines are fine.&lt;/p&gt;&lt;p&gt;Using &lt;code&gt;errgroup&lt;/code&gt;s and channels correctly can make goroutines less horrible to work with, but there are still many situations where it would just be so much eaiser to write correct code if we had structured concurrency.&lt;/p&gt;&lt;h3&gt;Deferring&lt;/h3&gt;&lt;p&gt;I strongly dislike the &lt;code&gt;defer&lt;/code&gt; statement. It makes it very hard to see where the code flow goes. There’s no alternative, sadly. I think that Python’s &lt;code&gt;with&lt;/code&gt; blocks are exemplary, as they effectively do the same thing but with two key advantages:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;you can use a &lt;code&gt;with&lt;/code&gt; block to scope the cleanup code to something other than a function&lt;/li&gt;&lt;li&gt;a &lt;code&gt;with&lt;/code&gt; block declares exactly what it scopes at the top, and always executes the same cleanup code at the end&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;Error handling&lt;/h3&gt;&lt;p&gt;Go tries to be too simple here. These days, when I write error handling code in Go, I always wrap the error at every level. That means that I’m effectively creating a stack trace as the error unwinds. Just I have to write the whole thing myself. Every single time. If Go had automatic stack traces and automatic error propegation, it would be infinitely more pleasant to use. Once again this is an area where &lt;a href=&quot;https://crates.io/crates/anyhow&quot;&gt;Rust excels&lt;/a&gt;.&lt;/p&gt;&lt;h3&gt;Type system-related issues&lt;/h3&gt;&lt;p&gt;Go has both typed &lt;em&gt;and untyped&lt;/em&gt; constants. Once again it’s a simple concept that turns out to be quite annoying. An untyped constant has no type, so it implicitly converts to whatever type is needed, but it has a default type that it falls back to. This means that any function that implements a comparison gets messed up&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-6&quot; id=&quot;fn-6-ref-1&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&lt;p&gt;Or how about taking a pointer to a value. In most languages (on the same level as Golang, that is) you can take a pointer of almost any value, but Go decides that all pointers must be writable. On the face of it this is a very attractice proposition because it eliminates a whole class of errors where we write to a pointer in rodata. But Go also doesn’t include any sort of Optional container&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-7&quot; id=&quot;fn-7-ref-1&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;, so any optional field becomes a pointer. Which means that all optional fields are pass-by-reference and mutable.&lt;/p&gt;&lt;p&gt;And that brings me to my next point, the language has no concept of optional parameters. This can sometimes be worked around with pointers but it makes it very painful to call the function because all arguments must be declared as variables beforehand. So most people use the &lt;a href=&quot;https://michalzalecki.com/golang-options-pattern/&quot;&gt;options pattern&lt;/a&gt; to get around this. Unfortunately this pattern requires loads of code, reduces discoverability of arguments and adds a level of indirection. If Go supported a simple, universal Optional type, that would mean we didn’t need to use pointers or options.&lt;/p&gt;&lt;p&gt;Still on pointers (wow I hate them), Golang supports nesting structs, and it can implicitly copy fields and methods from the inner struct to the outer one. Very neat. Here’s an example:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;go&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Animal&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;struct&lt;/span&gt; {
	&lt;span class=&quot;property&quot;&gt;Species&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;string&lt;/span&gt;
	&lt;span class=&quot;property&quot;&gt;Name&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;string&lt;/span&gt;
}

&lt;span class=&quot;keyword&quot;&gt;func&lt;/span&gt; (&lt;span class=&quot;variable&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Animal&lt;/span&gt;) &lt;span class=&quot;property function_method&quot;&gt;PrintName&lt;/span&gt;() {
	&lt;span class=&quot;variable&quot;&gt;fmt&lt;/span&gt;.&lt;span class=&quot;property function_method&quot;&gt;Sprintf&lt;/span&gt;(&lt;span class=&quot;string&quot;&gt;&amp;quot;My name is %s&amp;quot;&lt;/span&gt;, &lt;span class=&quot;variable&quot;&gt;a&lt;/span&gt;.&lt;span class=&quot;property&quot;&gt;Name&lt;/span&gt;)
}

&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Collar&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;struct&lt;/span&gt; {
	&lt;span class=&quot;property&quot;&gt;Label&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;string&lt;/span&gt;
}

&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Cat&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;struct&lt;/span&gt; {
	&lt;span class=&quot;type&quot;&gt;Animal&lt;/span&gt;
	&lt;span class=&quot;operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Collar&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;// pointer to make it optional; not all cats have collars.&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s ignore the convoluted example and focus on the fact that every &lt;code&gt;Cat&lt;/code&gt; contains an &lt;code&gt;Animal&lt;/code&gt; struct, which provides the cat’s name and species. This means that we can call &lt;code&gt;cat.PrintName()&lt;/code&gt; and it will Just Work™. There is also an optional &lt;code&gt;Collar&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;At this point, the cat may have a collar, which then contains a label. Golang, in an effort to be helpful, lets us call things like &lt;code&gt;cat.PrintName()&lt;/code&gt;, &lt;code&gt;cat.Species&lt;/code&gt; or (crucially) &lt;code&gt;cat.Label&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;If we try to access &lt;code&gt;Cat{}.Label&lt;/code&gt;, the entire program panics with a null pointer deference error, because &lt;code&gt;.Label&lt;/code&gt; is inside the &lt;code&gt;.Collar&lt;/code&gt; which is &lt;code&gt;nil&lt;/code&gt;. That’s despite the fact that the type that we accessed is not a pointer!&lt;/p&gt;&lt;p&gt;I’ll be the first to admit that this is quite a convoluted example because you should never have an embedded struct that is a nullable reference. It’s a very bad idea. But Golang permits it and raises no warnings about it, and I’ve seen it crop up in the wild.&lt;/p&gt;&lt;h3&gt;Standard library&lt;/h3&gt;&lt;p&gt;Due to the long-standing lack of generics (now resolved), Go’s standard library is very sparse, and what it does do is often overspecialised. I think generics is a very good case of when less is actually less – without it you have no chance of making useful, safe, reusable functions.&lt;/p&gt;&lt;h2&gt;Documentation&lt;/h2&gt;&lt;p&gt;And don’t even get me started on how poorly Golang is documented. If you search virtually any Go-related query, the first result is always some website called &lt;code&gt;golangdocs&lt;/code&gt;. I don’t trust it.&lt;/p&gt;&lt;h2&gt;Conclusion&lt;/h2&gt;&lt;p&gt;While I have mainly focused on the downsides of Go, most of these can be avoided if code is written to sensible standards. It’s unfortunate that these standards have no specification or documentation, and mostly have no linters available (because I’ve skipped the mistakes that have linters available).&lt;/p&gt;&lt;p&gt;On the whole I think Go is a decent language although it can be very frustrating at times.&lt;/p&gt;</description>
        <link>https://hack5.dev/blog/thoughts-on-golang/</link>
        <pubDate>Sun, 20 Jul 2025 14:08:00 +0000</pubDate>
        <guid>https://hack5.dev/blog/thoughts-on-golang/</guid>
      </item>
    
      <item>
        <title>Telegram bug in terminated sessions</title>
        <description>&lt;blockquote&gt;&lt;p&gt;&lt;a href=&quot;https://t.me/tgbetachat/673556&quot;&gt;Do you think that Telegram servers are coded by monkeys?&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;TL;DR&lt;/h2&gt;&lt;p&gt;A terminated session or a deleted account was still able to receive messages from active connections. Telegram fixed it as of 15~16 September 2021.&lt;/p&gt;&lt;h2&gt;Introduction&lt;/h2&gt;&lt;p&gt;The Telegram MTProto protocol is tricky, like its backend. Sometimes Telegram developers forget to implement some critical security controls when adding new features, in this case kicking out existing sessions. The same thing has happened before (or after) with missing rate-limiting in chat imports, although it was fixed very quickly after release.&lt;/p&gt;&lt;h2&gt;The vulnerability&lt;/h2&gt;&lt;p&gt;As the ability to invalidate logged in sessions or to kick out users from the application after their account got deleted are very old features, we can assume this vulnerability has been there for a long time.&lt;/p&gt;&lt;p&gt;But what is it about? Well, as you can read in the TL;DR, after a session is invalidated, if the connection has not been closed yet, Telegram will continue sending channels messages updates. Note that official clients always do close the connection, so the behaviour wasn’t obvious - everything appeared to work fine if you used an official client to test.&lt;/p&gt;&lt;p&gt;Note that &lt;code&gt;channel&lt;/code&gt; can mean a broadcast channel, a supergroup, a gigagroup or a local group, as they are all the same at API level. Therefore, you would not receive updates from private chats, bots, basic groups, or most importantly, the Telegram service account which sends login codes.&lt;/p&gt;&lt;h2&gt;POC&lt;/h2&gt;&lt;p&gt;When a session gets kicked out, Telegram sends an &lt;a href=&quot;https://core.telegram.org/constructor/updatesTooLong&quot;&gt;updatesTooLong&lt;/a&gt; constructor, which tells clients that they are supposed to call &lt;a href=&quot;https://core.telegram.org/method/updates.getDifference&quot;&gt;updates.getDifference&lt;/a&gt;, which will then give a &lt;code&gt;401 AUTH_KEY_UNREGISTERED&lt;/code&gt; RPC error, prompting the client to close the TCP connection.&lt;/p&gt;&lt;p&gt;However, if it is ignored by the client, and no more TL functions are called, Telegram will just continue sending you channel updates until connection is closed.&lt;/p&gt;&lt;p&gt;To exploit this, I used the &lt;a href=&quot;https://telethon.dev&quot;&gt;Telethon library&lt;/a&gt;. First, the script connected to the Telegram testmode environment and created an account for itself. Next, it immediately logged out of this account, meaning that it should be unable to read any new messages. Finally, it waited for incoming updates from Telegram (bypassing the built-in Telethon code, which made some extra RPC requests, breaking the code) and printed them.&lt;/p&gt;&lt;p&gt;The proof of concept code is very small (note that it no longer works, as the issue is resolved):&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;constructor constant variable&quot;&gt;asyncio&lt;/span&gt;
&lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;constructor constant variable&quot;&gt;telethon&lt;/span&gt;
&lt;span class=&quot;keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;constructor constant variable&quot;&gt;telethon&lt;/span&gt;.&lt;span class=&quot;constructor constant variable&quot;&gt;sync&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;constructor constant variable&quot;&gt;TelegramClient&lt;/span&gt;
&lt;span class=&quot;keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;constructor constant variable&quot;&gt;telethon&lt;/span&gt;.&lt;span class=&quot;constructor constant variable&quot;&gt;tl&lt;/span&gt;.&lt;span class=&quot;constructor constant variable&quot;&gt;functions&lt;/span&gt;.&lt;span class=&quot;constructor constant variable&quot;&gt;auth&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;constructor constant variable&quot;&gt;LogOutRequest&lt;/span&gt;
&lt;span class=&quot;keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;constructor constant variable&quot;&gt;telethon&lt;/span&gt;.&lt;span class=&quot;constructor constant variable&quot;&gt;tl&lt;/span&gt;.&lt;span class=&quot;constructor constant variable&quot;&gt;functions&lt;/span&gt;.&lt;span class=&quot;constructor constant variable&quot;&gt;updates&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;constructor constant variable&quot;&gt;GetStateRequest&lt;/span&gt;


&lt;span class=&quot;constructor constant variable&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;constructor function_builtin constant variable function&quot;&gt;TelegramClient&lt;/span&gt;(&lt;span class=&quot;constant_builtin&quot;&gt;None&lt;/span&gt;, &lt;span class=&quot;number&quot;&gt;12345&lt;/span&gt;, &lt;span class=&quot;string&quot;&gt;&amp;quot;0123456789abcdef0123456789abcdef&amp;quot;&lt;/span&gt;)
&lt;span class=&quot;constructor constant variable&quot;&gt;client&lt;/span&gt;.&lt;span class=&quot;constructor property constant variable&quot;&gt;session&lt;/span&gt;.&lt;span class=&quot;constructor property constant function_method variable&quot;&gt;set_dc&lt;/span&gt;(&lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;, &lt;span class=&quot;string&quot;&gt;&amp;apos;149.154.167.40&amp;apos;&lt;/span&gt;, &lt;span class=&quot;number&quot;&gt;80&lt;/span&gt;)
&lt;span class=&quot;constructor constant variable&quot;&gt;client&lt;/span&gt;.&lt;span class=&quot;constructor property constant function_method variable&quot;&gt;start&lt;/span&gt;(&lt;span class=&quot;constructor constant variable&quot;&gt;phone&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;apos;9996621234&amp;apos;&lt;/span&gt;, &lt;span class=&quot;constructor constant variable&quot;&gt;code_callback&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;lambda&lt;/span&gt;:&lt;span class=&quot;string&quot;&gt;&amp;apos;22222&amp;apos;&lt;/span&gt;)

&lt;span class=&quot;function&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constructor constant variable function&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;function&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable constructor property constant function_method function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;function&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constructor constant variable function&quot;&gt;telethon&lt;/span&gt;&lt;span class=&quot;function&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;constructor property constant variable function&quot;&gt;events&lt;/span&gt;&lt;span class=&quot;function&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable constructor property constant function_method function&quot;&gt;NewMessage&lt;/span&gt;&lt;span class=&quot;function&quot;&gt;())&lt;/span&gt;
&lt;span class=&quot;keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;constructor constant variable function&quot;&gt;raw&lt;/span&gt;(&lt;span class=&quot;constructor constant variable&quot;&gt;e&lt;/span&gt;):
    &lt;span class=&quot;constructor function_builtin constant variable function&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;constructor constant variable&quot;&gt;e&lt;/span&gt;.&lt;span class=&quot;constructor property constant variable&quot;&gt;text&lt;/span&gt;)

&lt;span class=&quot;keyword&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;constructor constant variable&quot;&gt;client&lt;/span&gt;:
    &lt;span class=&quot;constructor constant variable&quot;&gt;client&lt;/span&gt;.&lt;span class=&quot;constructor property constant function_method variable&quot;&gt;start&lt;/span&gt;()
    &lt;span class=&quot;constructor function_builtin constant variable function&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;constructor function_builtin constant variable function&quot;&gt;client&lt;/span&gt;(&lt;span class=&quot;constructor function_builtin constant variable function&quot;&gt;GetStateRequest&lt;/span&gt;()))
    &lt;span class=&quot;constructor function_builtin constant variable function&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;constructor function_builtin constant variable function&quot;&gt;client&lt;/span&gt;(&lt;span class=&quot;constructor function_builtin constant variable function&quot;&gt;LogOutRequest&lt;/span&gt;()))
    &lt;span class=&quot;constructor constant variable&quot;&gt;asyncio&lt;/span&gt;.&lt;span class=&quot;constructor property constant function_method variable&quot;&gt;get_event_loop&lt;/span&gt;().&lt;span class=&quot;constructor property constant function_method variable&quot;&gt;run_until_complete&lt;/span&gt;(&lt;span class=&quot;constructor constant variable&quot;&gt;asyncio&lt;/span&gt;.&lt;span class=&quot;constructor property constant function_method variable&quot;&gt;wait_for&lt;/span&gt;(&lt;span class=&quot;constructor constant variable&quot;&gt;client&lt;/span&gt;.&lt;span class=&quot;constructor property constant variable&quot;&gt;disconnected&lt;/span&gt;, &lt;span class=&quot;constant_builtin&quot;&gt;None&lt;/span&gt;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is an extract of the logs of what happened:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;DEBUG:telethon.network.mtprotosender:Handling update UpdatesTooLong
DEBUG:telethon.network.mtprotosender:Receiving items from the network...
UpdatesTooLong()
DEBUG:telethon.extensions.messagepacker:Assigned msg_id = 7003973880031307500 to PingRequest (7fea21e29fa0)
DEBUG:telethon.network.mtprotosender:Encrypting 1 message(s) in 28 bytes for sending
DEBUG:telethon.network.mtprotosender:Encrypted messages put in a queue to be sent
DEBUG:telethon.network.mtprotosender:Waiting for messages to send...
DEBUG:telethon.extensions.messagepacker:Assigned msg_id = 7003973880037489216 to MsgsAck (7fea21e29e80)
DEBUG:telethon.network.mtprotosender:Encrypting 1 message(s) in 60 bytes for sending
DEBUG:telethon.network.mtprotosender:Encrypted messages put in a queue to be sent
DEBUG:telethon.network.mtprotosender:Waiting for messages to send...
DEBUG:telethon.network.mtprotosender:Handling container
DEBUG:telethon.network.mtprotosender:Handling pong for message 7003973880031307500
DEBUG:telethon.network.mtprotosender:Handling update UpdateShort
DEBUG:telethon.network.mtprotosender:Handling update Updates
DEBUG:telethon.network.mtprotosender:Handling update Updates
DEBUG:telethon.network.mtprotosender:Receiving items from the network...
UpdateChannelUserTyping(channel_id=10812878, from_id=PeerUser(user_id=925104), action=SendMessageTypingAction(), top_msg_id=None)
UpdateNewChannelMessage(message=Message(id=2, peer_id=PeerChannel(channel_id=10812878), date=datetime.datetime(2021, 9, 4, 7, 15, 33, tzinfo=datetime.timezone.utc), message=&amp;apos;here is a sensitive message&amp;apos;, out=False, mentioned=False, media_unread=False, silent=False, post=False, from_scheduled=False, legacy=False, edit_hide=False, pinned=False, from_id=PeerUser(user_id=925104), fwd_from=None, via_bot_id=None, reply_to=None, media=None, reply_markup=None, entities=[], views=None, forwards=None, replies=MessageReplies(replies=0, replies_pts=3, comments=False, recent_repliers=[], channel_id=None, max_id=None, read_max_id=None), edit_date=None, post_author=None, grouped_id=None, restriction_reason=[], ttl_period=None), pts=3, pts_count=1)
&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Conclusion&lt;/h2&gt;&lt;p&gt;The flaw was reported to Telegram on 2021/09/04, and the bug was fixed by 2021/09/15.&lt;/p&gt;&lt;p&gt;I was offered a bounty but didn’t accept it because it came with an NDA, which would’ve forced me to abide by a series of rules that would’ve severely limited my freedom to disclose future vulnerabilities (whether responsibly or not) as well as trap me in an unnecessary, not to mention purposefully ambiguous, legal rat’s nest that could have been easily exploited to silence me in the future. I understand that Telegram needs to protect themselves from irresponsible security experts that disclose vulnerabilities in ways that dishonor the field, but forcing this kind of restriction onto people who spend their own time trying to find problems into their infrastructure and who attempt to proactively help fix them is an oxymoron, especially given that the economical compensation for such precious time, which Telegram never fails to boast, is locked behind the wall of signing this NDA. This sounds like a way of telling people “Thanks for reporting this! Now take this money, sign this paper here and shut up or we’ll sue you if you ever say a word about it”, which is counterintuitive given Telegram’s CEO Pavel Durov has always been very open about his despise for oppressive governments and organizations.&lt;/p&gt;&lt;h2&gt;Credits&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;Me for realising the bug and the PoC&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://t.me/telethonofftopic/744766&quot;&gt;@ShalmonAnandMate&lt;/a&gt; for reminding me of the issue&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://t.me/DavideGalilei&quot;&gt;@DavideGalilei&lt;/a&gt; and &lt;a href=&quot;https://t.me/nocturn9x&quot;&gt;@nocturn9x&lt;/a&gt; for helping with this write-up&lt;/li&gt;&lt;/ul&gt;</description>
        <link>https://hack5.dev/blog/telegram-sessions-bug/</link>
        <pubDate>Fri, 24 Sep 2021 07:52:52 +0000</pubDate>
        <guid>https://hack5.dev/blog/telegram-sessions-bug/</guid>
      </item>
    
      <item>
        <title>Turning off the RPi3B power LED in Fedora</title>
        <description>&lt;h2&gt;The problem&lt;/h2&gt;&lt;p&gt;Fedora doesn’t support the sysfs GPIO interface, and since it isn’t running the official raspbian kernel, you can’t use the GPU driver as listed on the RPi forums.&lt;/p&gt;&lt;h2&gt;What I tried first&lt;/h2&gt;&lt;p&gt;At first I tried enabling the configuration options in &lt;code&gt;/boot/efi/config.txt&lt;/code&gt;, but this had no effect, presumably due to the switch of the LEDs to the GPIO expander.&lt;/p&gt;&lt;h2&gt;Enabling GPIO&lt;/h2&gt;&lt;p&gt;Since the LED is on a GPIO extender, I wondered if Fedora would be able to see it via the GPIO. First follow &lt;a href=&quot;https://fedoraproject.org/wiki/Architectures/ARM/gpio#Using_the_GPIO&quot;&gt;Using the GPIO on Fedora Wiki&lt;/a&gt; to install &lt;code&gt;libgpiod&lt;/code&gt;, and run &lt;code&gt;sudo gpioinfo&lt;/code&gt;. At the end you should see this output:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;gpiochip1 - 8 lines:
	line   0:      &amp;quot;BT_ON&amp;quot;   &amp;quot;shutdown&amp;quot;  output  active-high [used]
	line   1:      &amp;quot;WL_ON&amp;quot;      &amp;quot;reset&amp;quot;  output   active-low [used]
	line   2: &amp;quot;STATUS_LED&amp;quot;        &amp;quot;ACT&amp;quot;  output  active-high [used]
	line   3:    &amp;quot;LAN_RUN&amp;quot;       unused  output  active-high 
	line   4: &amp;quot;HDMI_HPD_N&amp;quot;       unused   input  active-high 
	line   5:  &amp;quot;CAM_GPIO0&amp;quot;       unused  output  active-high 
	line   6:  &amp;quot;CAM_GPIO1&amp;quot;       unused  output  active-high 
	line   7:  &amp;quot;PWR_LOW_N&amp;quot;       unused   input  active-high 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Success! On line 7 you see the power LED.&lt;/p&gt;&lt;h2&gt;Turning off the LED&lt;/h2&gt;&lt;p&gt;It’s as simple as setting that pin to 1 - &lt;code&gt;sudo gpioset gpiochip1 7=1&lt;/code&gt; will turn off the LED.&lt;/p&gt;</description>
        <link>https://hack5.dev/blog/pi-3b-leds-fedora/</link>
        <pubDate>Wed, 31 Mar 2021 10:42:50 +0000</pubDate>
        <guid>https://hack5.dev/blog/pi-3b-leds-fedora/</guid>
      </item>
    
      <item>
        <title>Welcome to my new blog</title>
        <description>&lt;p&gt;Yeah, this is me now!&lt;/p&gt;&lt;p&gt;I spent a lot of money on this website, so I might actually use it.&lt;/p&gt;</description>
        <link>https://hack5.dev/blog/new-blog/</link>
        <pubDate>Sun, 06 Sep 2020 14:31:59 +0000</pubDate>
        <guid>https://hack5.dev/blog/new-blog/</guid>
      </item>
    
  </channel>
</rss>
