I discovered recently that Oracle Cloud has 4-core arm64 instances in their free tier. So I signed up for an account and set out to get an instance for myself. However, the instance creation failed stating that my chosen region did not have any capacity left for additional instances. A little bit of googling turned up this: https://www.reddit.com/r/oraclecloud/comments/on2e25/resolving_oracle_cloud_out_of_capacity_issue_and/. I didn’t read the linked article (which is apparently paywalled) but the poster had apparently posted a script that would attempt to acquire an instance in a loop until the attempt succeeded.

When creating the instance via Oracle’s web interface, I saw that I could get a terraform configuration for my chosen specs. I downloaded the terraform configuration, configured the provider using the official documentation, and asked Claude to generate a shell script that keeps attempting to apply the configuration until it succeeds. The script runs terraform apply -auto-approve in an infinite loop, exiting out with a code of 0 if the terraform command exits with 0.

And now I have for myself a 4-core arm64 instance.


I use Nix and Devenv to manage dependencies and build environments for all of my hobby projects. While I got the tutorial server up and running in a terminal relatively quickly, I had some trouble figuring out how to invoke it as a CLI from Claude Desktop’s MCP configuration. During development, I typically run nix develop --no-pure-eval to enter a devShell and then have access to all the packages I’ve declared a dependency on. In order to run node build/index.js from Claude Desktop, I needed the right config to get a nix run invocation to work. With some help from Claude itself, here’s the flake I ended up with:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    systems.url = "github:nix-systems/default";
    devenv.url = "github:cachix/devenv";
    devenv.inputs.nixpkgs.follows = "nixpkgs";
  };

  nixConfig = {
    extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=";
    extra-substituters = "https://devenv.cachix.org";
  };

  outputs = {
    self,
    nixpkgs,
    devenv,
    systems,
    ...
  } @ inputs: let
    forEachSystem = nixpkgs.lib.genAttrs (import systems);
  in {
    packages = forEachSystem (system: let
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      devenv-up = self.devShells.${system}.default.config.procfileScript;
      weather-server = pkgs.stdenv.mkDerivation {
        name = "weather-server";
        src = ./.;

        installPhase = ''
          mkdir -p $out/bin $out/lib
          cp -r build $out/lib/
          cp -r node_modules $out/lib/

          # Create a simple wrapper script
          cat > $out/bin/weather-server <<EOF
          #!${pkgs.runtimeShell}
          exec ${pkgs.nodejs_22}/bin/node $out/lib/build/index.js "\$@"
          EOF

          chmod +x $out/bin/weather-server
        '';
      };
      default = self.packages.${system}.weather-server;
    });

    devShells =
      forEachSystem
      (system: let
        pkgs = nixpkgs.legacyPackages.${system};
      in {
        default = devenv.lib.mkShell {
          inherit inputs pkgs;
          modules = [
            {
              # https://devenv.sh/reference/options/
              packages = with pkgs; [
                pkgs.nodejs_22
              ];

              languages.javascript = {
                enable = true;
                package = pkgs.nodejs_22;
                pnpm.enable = true;
              };

              processes = {
                app.exec = "node build/index.js";
              };
            }
          ];
        };
      });
  };
}

The corresponding entry in Claude Desktop’s config is:

{
    "mcpServers": {
        "weather": {
            "command": "/nix/var/nix/profiles/default/bin/nix",
            "args": [
                "run",
                "/Users/amey/Developer/mcp-server-tutorial#weather-server"
            ]
        }
    }
}
Tagged:

My employer has a few LLMs available internally via OpenAI-compatible endpoints. They also have an internal-only VSCode extension that’s configured to use those endpoints and provide code completion and a chat interface. Unfortunately, I don’t like using VSCode, plus I find the extension’s interface to be rather limiting. I haven’t fully embraced using an LLM as a coding assistant and instead prefer to use it as a rubber ducky for the most part, with the occasional round of “help me debug this thing that is hard to find on Google because Google sucks now”. So I figured that I’d run Open WebUI locally and configure it to the internal endpoint. Sadly, the endpoint uses a self-signed certificate, and Open WebUI doesn’t have an easy way to disable SSL verification. As such, it unceremoniously fails with

[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain=.

There’s one resolved issue on Github about it, but the recommended workarounds require source code changes. Plus the issue talks about httpx whereas the latest tag for their docker image uses aiohttp, neither of which support disabling SSL verification using environment variables. The change itself is rather simple, every ClientSession constructor needs to be given a connector with verify_ssl set to False, as follows:

aiohhtp.ClientSession(connector=aiohttp.TCPConnector(verify_ssl=False))

So I cloned their Github repo, made the change to backend/open_webui/routers/opeai.py and ran a docker build .. Which promptly failed because NodeJS ran out of heap memory. And then I thought - hey, this is python. There’s no compilation needed, the source file must be in the docker image in some form, what if I just modify that?

Turns out, the file is just copied to /app/backend/open_webui/routers/openai.py. My first attempt at this was adding a volume mount to the container and mounting the local routers folder to the right path. That failed with some error about the CACHE line in audio.py in the same folder, so they are doing some pre-processing somewhere. Or I got my docker tags mixed up, unclear. I didn’t dig any further because I got an even better idea: why not just generate a patch file with my changes and write a new Dockerfile that FROM’s the official docker image and applies the patch on top? And that’s exactly what I did. I ran git diff > patches/openai.patch to generate the patch file, and then wrote a new Dockerfile:

FROM ghcr.io/open-webui/open-webui:latest
COPY patches /opt/patches
RUN cd /app && git apply /opt/patches/openai.patch

After that, it was a matter of docker build . -t open-webui:patched and creating a docker-compose file that uses the new image from my local docker registry. And I had a working Open WebUI without OpenAI endpoint SSL verification!


Tail wags through the air,
butt follows closely.
Pure joy.

I’ve been having a rough few days. My partner thinks it’s because of the Daylight Savings switch, and maybe she’s right. It’s true that I’ve had similar rough patches at around the start of spring, and historically I’ve attributed them to the sudden change in the pace of life around me. Being an introvert partnered with an extrovert, March usually feels like a whirlwind as my partner seemingly emerges out of her winter hibernation and starts going out everyday, either maintaining old relationships or building new ones. And I usually stick with what I’d been doing during the winter, which is either games or hobbies. But this year has been different - I’ve been just as outgoing (in my own way) this winter, skiing at least one day every week, meeting with my close friends, and deepening some of my shallower friendships. And yet, here I am in the second week of March, feeling like life’s been beating me up and stealing my lunch money.

Maybe part of this is the general state of the world. We’re going through times that feel historical, and in a sense they are. If this book is to be believed, we’re simply in the crysis part of a cycle that’s been happening for several centuries, and while it’s not the “end of times”, there are tough times looming for all of us. Every conversation I’ve had recently has visited this topic at some point, whether through the lens of family of my Canadian friends that are boycotting American goods, or friends whose investments continue to lose value, or the dismantling of various government apparatus and bring independent government organizations under the president’s control, or the efforts the Washington state government is undertaking to undermine the efforts of ICE and border control.

Today felt especially rough for several reasons. At the end of the work day, I set out to go night skiing, only to end up being forced to turn back because the pass to the resort was closed due to inclement weather. After returning home, I decide to take our dog on a walk, hoping to tire him out a bit more. However, as soon as we came back home from the walk, he bounded up the stairs, picked up one of his squeaky toys and dropped it in my general direction, his tail wagging furiously in an unmistakeable invitation to “Play!”. Annoyed by his distinct lack of tiredness, I snapped “I’m not playing with you” as I walked past him upstairs, and out the corner of my eye I saw his tail stop wagging. I immediately felt sad that I’d made the dog sad but also resentful that he didn’t want to leave me alone. Couldn’t he see that I was feeling bummed? And then I thought - maybe he does see that I’m feeling down and that’s exactly why he’s inviting me to play with him. Maybe he recognizes that there is no better balm for a bruised soul than a vigorous game of tug and keep-away. So that’s exactly what I did; I came back downstairs, play-bowed to him and played with him for several minutes. And you know what? I think he was right. I do feel better. I’ll make an effort to listen to his advice more often.

Tagged:

Old Dog, New Tricks

Mar 9, 2025

One of my favorite quotes of all time is this definition of insanity:

Insanity is doing the same thing again and again and expecting different results. It’s thinking - “this time it’s going to be different”.

I don’t know where the original quote comes from (Benjamin Franklin? Gandhi?) but I heard it for the first time from a villain in the game “Far Cry 3”. Here’s the clip if you want to watch it, I still get goosebumps from the voiceacting and motion capture: https://www.youtube.com/watch?v=rKMMCPeiQoc.

I used to be a prolific blogger. And then I somehow fell out of the habit of writing regularly, and instead got in the habit of tinkering with my blog.

  • The blog started over on https://blogger.com
  • I moved it over to Wordpress
  • I moved it from Wordpress to Poet
  • I moved it from Poet to Jekyll hosted on Github Pages
  • I moved it from Jekyll to Hugo, hosted on S3
  • I changed the deployment mechanism from Codeship to AWS CodePipeline
  • I changed the deployment mechanism to Cloudflare Pages

There was a long break between each of these and the previous post. I did not write anything for a long time, realized that I hadn’t written anything for a long time, made a long blog post about how I’d made the blog better and easier to write, and then didn’t write anything else for another long stretch.

With that out of the way, let me tell you about how I’ve now made the blog much better and a lot less trouble to write…

I’m only half kidding. The blog’s backend is mostly unchanged - it still uses Hugo, it’s still hosted on Cloudflare Pages. I did try to use ox-hugo to export subtrees out of a single large org-mode file into individual posts, but inter-post links via that method have apparently been broken for at least a year. I then opted to use the method outlined in https://yejun.dev/posts/blogging-using-denote-and-hugo/ with a few modifications to roll my own solution that exports from specially-marked Denote notes to Hugo markdown posts. The elisp code I hacked together with some help from https://claude.ai isn’t particularly well-written or generalizable, but it works well enough to do what I need it to do (mark a note as a blog post, export it, export all marked notes from directory) and stays out of my way otherwise. Here’s all of the code to make that happen:

;; Adapted from https://yejun.dev/posts/blogging-using-denote-and-hugo/
;; Converts denote links to hugo's relref shortcodes to generated files.
(advice-add 'denote-link-ol-export :around
            (lambda (orig-fun link description format)
              (if (and (eq format 'md)
                       (eq org-export-current-backend 'hugo))
                  (let* ((path (denote-get-path-by-id link))
                         (export-file-name (ameyp/denote-generate-hugo-export-file-name path)))
                    (format "[%s]({{< relref \"%s\" >}})"
                            description
                            export-file-name))
                (funcall orig-fun link description format))))

;; Add advice around org-export-output-file-name so that I can generate the filename from denote frontmatter
;; rather than needing to add an explicit export_file_name property.
(advice-add 'org-export-output-file-name :around
            (lambda (orig-fun extension &optional subtreep pub-dir)
              (if (and (string-equal extension ".md")
                       (ameyp/denote-should-export-to-hugo))
                  (let ((base-name (concat
                                    (ameyp/denote-generate-hugo-export-file-name (buffer-file-name))
                                    extension)))
                    (cond
                     (pub-dir (concat (file-name-as-directory pub-dir)
                                      (file-name-nondirectory base-name)))
                     (t base-name)))

                (funcall orig-fun extension subtreep pub-dir))))

(defvar ameyp/denote--hugo-export-regexp "hugo_export[[:blank:]]*:[[:blank:]]*"
  "The frontmatter property for indicating that the note should be exported to a hugo post.")

(defun ameyp/denote-generate-hugo-export-file-name (filename)
  "Generates a hugo slug from the supplied filename."
  (let* ((title (denote-retrieve-filename-title filename))
         (date (denote--id-to-date (denote-retrieve-filename-identifier filename))))

    (concat date "-" title)))

(defun ameyp/denote-should-export-to-hugo ()
  "Check whether the current buffer should be exported to a published hugo post."
  (save-excursion
    (save-restriction
      (widen)
      (goto-char (point-min))
      (if (re-search-forward ameyp/denote--hugo-export-regexp nil t 1)
          (progn
            (let ((value (buffer-substring (point) (line-end-position))))
              (or (string-equal value "t")
                  (string-equal value "true"))))))))

(defun ameyp/goto-last-consecutive-denote-property-line ()
  "Move point to the last consecutive line at the beginning of the buffer that starts with '#+'"
  (interactive)
  (goto-char (point-min))
  (let ((last-prop-line (point-min)))
    (while (looking-at "^#+")
      (setq last-prop-line (point))
      (forward-line 1))
    (goto-char last-prop-line)
    (if (looking-at "^#+")
        (beginning-of-line)
      (message "No property line found"))))

(defun ameyp/org-hugo-mark-for-export()
  "Inserts a frontmatter property to mark the denote file for export to a hugo post."
  (interactive)
  (save-excursion
    (save-restriction
      (widen)
      (goto-char (point-min))
      (if (re-search-forward ameyp/denote--hugo-export-regexp nil t 1)
          ;; Found an existing property, set it to t.
          (progn
            (delete-region (point) (line-end-position))
            (insert "t"))
        ;; No existing property found, go to the end of the frontmatter and insert the property.
        (ameyp/goto-last-consecutive-denote-property-line)
        (goto-char (line-end-position))
        (insert "\n#+hugo_export: t")
        )
      ))
  )

(defun ameyp/org-hugo-export ()
  "Export current buffer to a hugo post."
  (interactive)
  (if (ameyp/denote-should-export-to-hugo)
      (let ((org-hugo-section "post")
            (org-hugo-base-dir "~/Developer/wirywolf.com")
            (org-hugo-front-matter-format "yaml"))
        (org-hugo-export-wim-to-md))
    (message (format "Not exporting %s" (buffer-file-name)))))

(defun ameyp/org-hugo-export-marked-files ()
  "Export all marked files in dired buffer to hugo posts."
  (interactive)
  (let ((org-hugo-section "post")
        (org-hugo-base-dir "~/Developer/wirywolf.com")
        (org-hugo-front-matter-format "yaml"))

    (save-window-excursion
      (mapc (lambda (filename)
              (find-file filename)
              (ameyp/org-hugo-export))
            (dired-get-marked-files))
      )))

It mostly works, except if I need to embed hugo shortcodes inside a source code block, like in the above block. I haven’t figured out how to escape them automatically during the conversion process (I rely on ox-hugo’s markdown exporter) so for now, any hugo shortcodes must be manually escaped in the generated markdown as described in this post: https://liatas.com/posts/escaping-hugo-shortcodes/