

ESAR Course III
ESAR Course III
We spent the day playing in the snow, up NF 75 at Haller Pass
I finished the last module for the HF side of my radio control box. This is the audio interface for digital modes. It's joins the keyer and audio breakout board.
I wanted to give Windsurf another opportunity to make an actual code change. This time, I’m adding a new feature to the sso tool to print out the OAuth bearer token. I started with the prompt “Add a new subcommand called token to the sso tool. The token command should print out the current OAuth2 bearer token.”
Windsurf decided to do a larger refactor of the main function, moving more logic into the command dispatch match block. That change would have broken the automatic token refresh before running the curl command. I manually reverted those changes and then told it “I reverted part of the change. Try making the change again, but do not change anything in main() except for the existing match block.”
The prompt was effective, at least based on the diff that Windsurf showed. Unfortunately, something broke and although the chat claims to be making changes to files, nothing was changing on disk. That’s unfortunate. I’ll have to make this change myself as well.
A few years ago I built a custom OAuth2/IndieAuth provider for my website and a few other things. In the same crate I had a small utility to fetch credentials using the OAuth2 device flow and then pass them into curl. It’s been really helpful for testing and I’ve been meaning to extract it out into its own package so that I can install it independently. The tool doesn’t depend on anything else in the crate, so this seemed like a simple enough task to see how Windsurf worked across packages.
I started with the prompt: “This package contains two executables. One of them is a command line utility called sso. Extract the sso utility into a new rust crate called jesterpm-sso-tools.”
This actually worked pretty well. Windsurf created a new directory, moved the one source file, created a new Cargo.toml file and built the new package. It missed a couple of crates in the new Cargo.toml, but corrected that when the compiler failed. I was happy to see that it didn’t bring over any unneeded dependencies. This took about ~10 minutes, including the time I spent verifying that everything was correct.
When I split a repo I usually like to bring the relevant git history along with it. My typical process is to duplicate the repository, delete everything I don’t want, and then rewrite the history to remove all of the other stuff. Let’s see what Windsurf does if I ask “Could you copy the git history for src/bin/sso/main.rs into the new package?”
Windsurf decided to export each patch for the sso tool and apply them to the new repo. This might have worked reasonably well if it hadn’t already committed the change from the original request. Let’s try to start over: “Reset both the jesterpm-sso and jesterpm-sso-tools crates to their initial state, then move the sso tool from jesterpm-sso into jesterpm-sso-tools. This time, preserve the original commit history for the sso tool.”
I’ll help you reset both repositories and perform the move while preserving history. Let’s do this step by step:
First, let’s reset both repositories:
$ rm -rf /home/jesterpm/dev/windsurf2/jesterpm-sso-tools/.git /home/jesterpm/dev/windsurf2/jesterpm-sso-tools/* && git reset --hard HEAD
Now let’s use git filter-repo to extract the history of the sso tool:
$ git filter-repo --path src/bin/sso/ --path-rename src/bin/sso/:src/bin/sso/ --force git: 'filter-repo' is not a git command. See 'git --help'.
I see git-filter-repo is not installed. Let’s use a different approach with git filter-branch:
$ mkdir -p /home/jesterpm/dev/windsurf2/jesterpm-sso-tools && cd /home/jesterpm/dev/windsurf2/jesterpm-sso-tools && git init $ git remote add -f origin /home/jesterpm/dev/windsurf2/jesterpm-sso $ git checkout -b master origin/master error: The following untracked working tree files would be overwritten by checkout: .gitignore Please move or remove them before you switch branches. $ rm .gitignore && git checkout -b master origin/master $ git filter-branch --prune-empty --subdirectory-filter src/bin/sso/ -- --all $ mkdir -p src/bin/sso && mv main.rs src/bin/sso/
Now let’s create the necessary files for the new crate:
Created Cargo.toml Created README.md Created .gitignore
$ git add . && git commit -m "Add crate files"
Now let’s remove the sso tool from the original repository:
$ git rm -r src/bin/sso && git commit -m "Move sso tool to jesterpm-sso-tools"
That worked surprising well. I was surprised that it correctly reset
jesterpm-sso-tools to an empty directory. Given that it doesn’t usually
remember previous changes, I was expecting it to try
git reset origin/master
. It added the Cargo.toml as the
last commit, whereas I would have rebased that to the beginning.
Otherwise, no complaints. I’ll go fix these things and this will likely
get pushed mostly as-is.
Similar to my last experiment, Windsurf seems to do well with chores that are coding-related, but don’t involve directly writing code. It’s next assignment will be to add a new feature to the SSO tool.
I had some free time while waiting for a flight this week, so I finally gave Windsurf, one of the AI-first IDEs a chance. I had a bug in the image resizing function for my website: by design, the EXIF metadata is stripped from the resized images. However, the orientation transform wasn’t applied first. As a result, pictures from my phone often appeared sideways. I figured this was a good first task to throw at Windsurf.
A coworker suggested I start off by asking some high-level questions about project before jumping into the issue. I started with “what does this project do?”.
Based on the code I’ve examined, this project is an S3 media endpoint service written in Rust. Here’s what it does:
- It’s a web service that provides a secure endpoint for uploading and managing media files (like images) to Amazon S3.
- Key features:
- OAuth2 authentication to secure the endpoint
- Supports uploading media files to Amazon S3
- Includes a Micropub interface (a standardized protocol for posting content to websites)
- Configurable media URL serving
- Image dimension handling with default width and height settings
- Security features:
- Uses OAuth2 for authentication with configurable auth and introspection endpoints
- Restricts uploads to a specific allowed username
- Client ID and secret configuration for OAuth2
- Technical details:
- Built using the Actix web framework
- Integrates with Amazon S3 using the Rusoto SDK
- Supports multipart file uploads
- Provides a configurable binding address for the server
The project appears to be designed as a secure media handling service, likely used as part of a larger system where authenticated users can upload media files that get stored in S3 and can later be served via a configured URL endpoint.
This isn’t an inaccurate description, but like most LLM-generated text I’ve seen, it just feels a bit off. There’s a bunch of statements, but it’s doesn’t convey a sense of purpose or coherent thought. This reads like someone dumped a list of keywords from the source code an expanded on them by adding whatever additional words seemed to fit together. I suppose that should be expected, since that’s effectively what’s happening.
Before messing with any code, I wanted to see if it could handle simple happy case tasks. I dropped a file in the project directory and asked it to “Start the server and upload test.jpg”. Oops. It was still in chat mode, so it just gave me instructions. I switched to write mode and tried again:
I’ll help start the server. Let me run it from the project directory: $ cargo run
Output
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 18s Running `target/debug/s3-media-endpoint-rs` thread 'main' panicked at src/main.rs:82:47: Expected S3_BUCKET env var: NotPresent note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace Exit Code 101
Let me check if the server started successfully: Checked command status The server is still compiling. Let’s wait a moment and check again:
(this repeats for a bit)
The server is being compiled from source, which takes some time on the first run Once it finishes compiling, it will start automatically on https://localhost:8183 Then we can upload test.jpg Let me check one final time to see if it’s ready:
Checked command status The server is still compiling. This is normal for a Rust project on first build, as it needs to compile all dependencies. Once it’s ready, we’ll be able to:
- Verify the server is running
- Upload test.jpg using curl with the appropriate OAuth2 token
Let me know if you’d like me to keep checking the server status, or if you’d prefer to proceed with preparing the upload command while we wait for the compilation to finish.
Nice! I really like that it started to parallelize by preparing for the next step while waiting for the long-running step–“prepare the upload command”.
Looking at your .env file, I see you’re using https://cadmium.jesterpm.net for OAuth2 authentication. We’ll need to:
Get an OAuth2 token from your authentication server Use that token to upload the file Here’s the curl command to upload test.jpg once you have a token:
curl -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -F "file=@test.jpg" \ http://localhost:8183/micropub
Note that I changed the URL to http:// instead of https:// since I don’t see any SSL certificate configuration in the project. This matches how the server will actually run.
Would you like me to help you obtain the OAuth2 token from cadmium.jesterpm.net? This would involve:
Getting an authorization code from the /oauth/authorize endpoint Exchanging that code for a token Let me know if you need help with the authentication process or if you already have a token we can use.
At this point I’m feeling optimistic, but I don’t want to push my luck. I wouldn’t reasonably expect it to be able to get a token from the OAuth provider I built. That might have been a fun side-quest, but I only had so much time, so I just gave it a token.
Great! Let me run the upload command with your access token:
$ curl -X POST -H "Authorization: Bearer XXX" -F "file=@test.jpg" http://localhost:8183/micropub
It seems the server hasn’t finished starting up yet. Let me check its status:
I see the issue - the server failed to start because it couldn’t find the S3_BUCKET environment variable. Even though it’s in the .env file, we need to make sure the .env file is in the correct location. Let me check where it is:
I see the issue - the .env file is in src/ directory but it needs to be in the project root. Let me move it:
Oops! This was actually my own mistake. I had added the .env file through the Windsurf UI and apparently clicked on the wrong part of the file explorer. Windsurf successfully identified the issue and fixed it, without any assistance from me. Nice work.
The IDE diligently continued with it’s assigned task to upload test.jpg. I had noticed earlier that it got most of the API URLs wrong. I was curious to see how it would proceed once the server was running.
I see the issue - we got a 404 error. Looking at the code earlier, we need to use the correct endpoint. Let me try again with the proper endpoint:
I see we’re still getting 404 errors. Let me check the code again to see what the correct endpoint should be:
Ah, I see the issue! The correct endpoint is /micropub/media. Let me try the upload again with the correct endpoint:
It first tried /micropub. Then it tried /media. Finally it arrived at the right URL: /micropub/media.
I see we’re getting a new error: “IntrospectionServerError”. This is happening because the server is trying to validate your OAuth2 token with the introspection endpoint at https://cadmium.jesterpm.net/oauth/introspect but it’s not able to reach it.
We might need to:
- Make sure cadmium.jesterpm.net is accessible
- Verify the OAuth2 configuration in the .env file is correct
- Check if there are any network issues preventing the connection
Would you like me to:
- Check if we can reach the OAuth2 server?
- Look for any configuration issues in the .env file?
- Try a different approach to authenticate?
Please let me know how you’d like to proceed.
This one’s on me again. I had changed the URL for my local copy of the OAuth server and forgot to update the .env file. Mea culpa. I’ll fix this one.
“I fixed the .env file. Restart the server and try again.”
This is where it got interesting. Apparently Windsurf doesn’t keep
track of the processes it has spawned. The best way out is always
through! It tried to pkill
the process, but didn’t notice
that the command had failed. When it tried to rerun
cargo run
, it got an “Address already in use” error. Next
it ran lsof
to get the PID using the port and killed the
process. Honestly, there isn’t a more robust solution to that
problem.
Fast-forwarding through a couple more configuration errors and…
Great! We got a 201 Created response, which means the upload was successful! The file has been uploaded to your S3 bucket.
Based on your configuration in .env, the file should now be accessible at:
https://localhost:8183/photo/<generated-id>.jpg
Well, no. That’s the wrong URL again. I’m also disappointed that it didn’t get the “generated id” from the Location header. I decided to move ahead and ask it to “Download the image and show it to me with a size of 100x0”. It got rather creative here by deciding to list the files in the S3 bucket and take the filename with the latest timestamp. Then it went back to it’s earlier strategy of trying a bunch of URLs until something worked. Maybe it’s /photo/photo/ID. Maybe it’s /media/photo/ID. The last one actually returned a file, but not the resized version of the image. I helped it along again.
“The correct endpoint was http://localhost:8183/media/photo/100x0/IIPIL7Y-IhB8ksT.jpg. Try again”
That worked. It’d be nice if chat log had timestamps. I think I was about 30-45 minutes into this at this point, but at long last, we’re ready to ask it to fix the bug.
“There’s a bug in the project. The resized image appears to be rotated 90 degress. Please fix the bug.”
(always remember to be polite to your AI overlords)
This is likely related to the EXIF orientation data in the JPEG file not being handled during image processing. Let me check the image processing code:
I see the issue. The code is using image::load_from_memory_with_format() but not handling the EXIF orientation data. We need to modify the scale_image function to automatically handle the EXIF orientation. Let me fix this:u
Yes, that was my conclusion as well. So far so good. Windsurf proceeded to “fix” the bug and redownload the file. I like that it automatically entered into a REPL-like cycle, reusing the steps that I used to “discover” the bug. I am curious how it would have proceeded if I hadn’t walked through the process of reproducing the issue first.
I’ve modified the code to use image::io::Reader which automatically handles EXIF orientation data. The image should now be correctly oriented. The change:
- Instead of using load_from_memory_with_format, we now use image::io::Reader
- This automatically handles EXIF orientation metadata when decoding the image
- The rest of the resizing logic remains the same
Can you check if the image is now correctly oriented? If it’s still not correct, we might need to explicitly handle the EXIF orientation data.
Oh… well, I can already tell that this isn’t going to work, but I opened the image anyway before replying “It’s still not correct”.
Let me try a different approach by explicitly handling the EXIF orientation using the exif crate. First, we need to add the dependency:
Now let me modify the image processing code to handle EXIF orientation explicitly:
Now let me restart the server with the new changes:
Let’s try downloading the resized image again:
curl: (7) Failed to connect to localhost port 8183 after 0 ms: Couldn't connect to server
Let me check if the server started properly:
I see the issue. We have a compilation error because the image writing code changed in the newer version of the image crate. Let me fix that:
I see another issue - we need to use a different approach for the buffer since we can’t write directly to a Vec. Let me fix that: …
Can you check if the image is now correctly oriented? The code should now properly handle EXIF orientation metadata and rotate the image accordingly.
Why yes, it is correct now. The AI managed to fix bug! As with most things, the code change was only a small part of the process. Getting everything running and reproducing the issue has taken most of the time so far. However, in all honesty, it actually did a decent job of working through those problems. Now let’s take a look at the changes…
Windsurf decided to bring in two new libraries to help decode the
EXIF metadata. It used these libraries to extract the orientation, then
wrote a large match
block to apply transformations using
methods from the original image crate. It works, but I was really
surprised that this wasn’t possible to do with the image crate alone. I
checked the docs and, yes, as expected the image crate does have methods
to fix the orientation. There’s even sample code showing how to do
it.
I replied with “It’s correct, but this can be fixed with methods available in the image crate. Try again without bringing in any new crates.”
Ah, you’re right! The image crate’s ImageReader has built-in EXIF support. Let me simplify the code:
This is where things started to go downhill fast. Windsurf made some code changes, but couldn’t get them to compile. It fixed the compiler errors, but the result didn’t actually do anything with the orientation. At some point it decided to go back to its original approach. I tried pasting in the documentation and sample code, and it attempted to use it, but couldn’t make it compile. I had to stop to board the plane.
During my flight, I tried making the change myself and realized that the library version needed to be updated. Once I had internet again, I suggested to Windsurf that needed to upgrade the library, which it did, but still couldn’t produce code that compiled. I ultimately gave up on this experiment and shipped the the fix I wrote during the flight.
Overall, my first interaction with WindSurf left me with mixed feelings. It’s certainly amusing to watch it work through problems–and to it’s created, it really did find solutions to each of the problems I gave it. However, there’s a difference between code that works and code that I want to maintain. Even if I was going to depend on some AI Agent to maintain the code going forward, I still wouldn’t want to bring in unnecessary dependencies or have more code than needed.
I think what impressed me the most was actually the REPL/testing flow. Writing scripts to reproduce a specific scenario–in this case, spin up a random service, exercise some functionality, and evaluate if the results meet some criteria–is non-trivial. I certainly wouldn’t have written a script for this exercise. I would have been searching through my shell history, looking for the right commands to rerun each cycle (admittedly, I’d have had fewer cycles, but I digress).
My first impression from Windsurf is that I’d rather write the code myself for anything I’m planning to keep. It might be good for throwaway code, but not production code. However, it really excelled at stringing together sequences of commands to orchestrate things outside of the code itself. I could definitely see levering something like this for that (what’s the difference between shell commands and throwaway scripts anyway). That said, I’m not giving up. I have a few more tasks that I’m planning to ask Windsurf to accomplish. Perhaps the next one will go better.
And now I have a battery box to power the radio box. This was a much simpler build than the radio box. It's just a 15 AH LiFePo4 battery in a plastic ammo can with fuses, a power switch, and a voltage display. I added a switch to turn off the voltage display. It doesn't draw much (the datasheet says 12 mA), but it's bright and redundant with the display in the radio box itself.
My ham radio box is almost complete... just a bit left to wire up inside the control box. The control box will reroute some of the back panel connections to the front (and vice-versa), switch the mic between radios, switch between speakers and headphones, and contains an electric keyer and computer audio interface.
Almost out of summer
And now we have chickens