Posts in Go (20 found)

Concurrent Servers: Part 8 - Go

This is part 8 in a series of posts on writing concurrent network servers. In this part, we'll switch to Go and see how it tackles the challenges described earlier in the series. All posts in the series: This post assumes a basic familiarity with the Go programming language. As before, we'll start with a sequential server for the basic state machine protocol presented in part 1 . This is the main function: As in the previous parts, the server is "infinite"; it never stops serving new connections until it's explicitly killed. This is the function implementing the protocol for a single client; it takes a net.Conn value that represents a socket with a client connected on the other end: Rather than directly exposing OS threads, the Go runtime implements its own M:N scheduling of lightweight goroutines on top of OS threads. Using goroutines in Go is cheap - both in terms of syntax and developer effort, and in terms of system resources . Here's a version of our serial protocol server that serves clients concurrently by launching a goroutine for each client. The part of the code that's different from the previous sample is highlighted: The concurrent modification in this case is particularly simple because the server is infinite; there's no point waiting for these goroutines to finish (and hence no need for a sync.WaitGroup ). The parameters for server.ServeSerialProtocol are lexically captured from the enclosing scope and its return value is handled by the surrounding closure. Because goroutines are very cheap, this server is very unlikely to run out of resources due to launching too many goroutines; in fact, it will probably run out of something else - like file descriptors for sockets - first. However, sometimes it's still useful to limit the degree of concurrency - even in Go, and we'll discuss some approaches to do so in the following sections. Here are some scenarios in which it makes sense to limit the degree of concurrency in Go programs, even though goroutines are cheap to launch and operate: Let's switch to the primality testing server from part 4 for the rest of the post, because it represents a somewhat more realistic workload. As a reminder: the server receives numbers, simulates blocking by sleeping, and returns "prime" or "composite". The unbounded one-goroutine-per-client version looks almost identical to the previous code sample, except that the goroutine invocation calls another function: Where ServePrimeProtocol is [1] : The simplest way to limit concurrency in Go is by using a counting semaphore, implemented with a channel: The channel sem serves as a semaphore; note that it's a bounded channel with a maximal size. A token is acquired by sending to the channel, and released by receiving from the channel. When the channel is full, the send operation sem <- struct{}{} blocks until a token was removed by some other goroutine [2] . The type of the channel is struct{} which means "empty", or "no data". This is idiomatic in Go for channels that are used solely for their semantics, not to send/receive any actual data. Since launching goroutines is cheap and limiting concurrency is easy as shown above, the "worker pool" pattern is often unnecessary for scenarios like our server. Still, it has occasional uses (such as when workers have to maintain some non-trivial state across tasks) so it's worth discussing it here. Here's a variant of our primality testing server that uses a worker pool: A fixed number of worker goroutines is launched; these goroutines all receive "jobs" from the same channel. In the Accept loop, each client connection is sent to this channel as a new job and is picked up by the next available worker. As mentioned before, you would typically see a sync.WaitGroup somewhere to ensure clean shutdown of goroutines, but in our case it isn't necessary because we have a server that never exits. Do programmers have to resort to async / event-driven programming in Go? In my experience, almost never. Go was designed from the bottom up to be suitable for large-scale concurrency; goroutines are very cheap to create, have a tiny memory footprint and switching happens very quickly, all in user space. Measurements I ran back in 2018 have shown switching times of ~170 ns, as compared to 1-2 microseconds for threads on Linux. Moreover, Go already uses event-driven loops like epoll underneath for I/O. Goroutines that wait for I/O like sockets are effectively "parked" and consume no resources (beyond their small memory footprint); they are woken up by Go's runtime when their I/O descriptors are ready - this is very similar to how asynchronous programming works! That said, some people certainly do try to stretch their resources even more with direct asynchronous programming in Go when millions of streams are handled concurrently. All I'll say is that this is very rare, and an overwhelming majority of users never have to do this. In 2018, I wrote a post named Go hits the concurrency nail right on the head , and after several more years of active coding, I fully stand behind that statement. Go is extremely powerful and ergonomic for concurrent programs; while other environments go to great lengths to implement async-await style event loops in libraries, in Go it's already baked into the core language and runtime. You want event-driven I/O with very lightweight green threads that can also execute blocking tasks without worrying about the function coloring problem ? Go has you covered. All the code for this post is available on GitHub . Careful readers will note two issues with this code: (1) the protocol assumes the complete number is read from the socket in a single conn.Read call, and there's no framing - separation between distinct numbers; (2) the prime checking loop uses i*i which may overflow for large numbers. These issues are consistent across all the versions of the prime server in C, Python, JavaScript and Rust in earlier parts, because my focus was on the simplest possible code to demonstrate a point about concurrency. Part 1 - Introduction Part 2 - Threads Part 3 - Event-driven Part 4 - libuv Part 5 - Redis case study Part 6 - Callbacks, Promises and async/await Part 7 - Rust Part 8 - Go (this part) Tasks may be compute intensive, and the CPU capacity of any server is inherently limited. If too many concurrent goroutines compete for limited CPUs, they will all make very little progress. It may make more sense to have fewer tasks that complete in a reasonable time. Protecting potentially limited downstream resources, such as concurrent DB connections or other services. For example, if the server has to send requests to other services for each task, and these are rate-limited, concurrency will have to be carefully managed. Security reasons when work is dictated by clients; malicious clients can overload and crash a service that's too eager to serve, making it unavailable for legitimate clients.

0 views
matduggan.com 2 weeks ago

OTel Isn't Going Well (And I Made A Spreadsheet About It)

For years now one of the most reliable complaints I hear when I try to drag a team off their vendor specific SDK and onto OpenTelemetry is some variation of: "why does it seem like this isn't done yet?" Vendor SDKs for observability are, to put it charitably, idiot-proof. You install the thing, dashboards just load data, someone else worries about how all those pieces fit together, and you get on with your life. OpenTelemetry, by contrast, greets you at the door with a lot of "experimental" stamps and roughly six different ways to accomplish any given task. In OpenTelemetry's defense this was never what they were going for as a project. I've always respect that they stuck to their guns by attempting to build a truly vendor agnostic system that really doesn't care what you do with the data. I have never gotten a sense of a vendor being strongly preferred with OTel, which is quite the feat considering how lucrative and contentious the observability ecosystem was. Also considering that the maintainers of this project are largely employed by exclusively those companies. As the years wore on, I started to get nervous. Conversations in the semantic-conventions repo drag on and on and on. Different languages had dramatically different stories. Golang and Dotnet were first class citizens, but other languages lagged years behind the others. I started asking a lot of probing questions before recommending OpenTelemetry to smaller teams who didn't have the time, budget, or emotional bandwidth for it. Auto-instrumentation was genuinely magical, but the cliff between "auto-instrument works" and "now I have to manually instrument something" was steep enough that you owed people a warning before you pushed them off it. This narrative has been going on for awhile in the observability space, a vague sense of "something is wrong in Otel-land". But let's try to generate some actual data here. Is there an actual problem, or is this something where the perception by the community of slow progress is imaginary? Is the problem not enough maintainers, too big of a scope, or something in-between? My guess when I started was "oh this is your classic open-source bit off more than they can chew". Not enough maintainers, not enough budget. Now there is some of that, but there's also something else going on. The actual problem happening inside of OpenTelemetry is a three way crash. You have a binary stability gate which, when combined with a very small bench of actual maintainers means there is understandable worry about marking a feature not experimental then add on just a massive scope of languages and frameworks they are attempting to cover. This creates a perfect storm where there is an incentive to argue about potential problems a feature might create since once it is locked in and shipped as stable you can never change them. So OpenTelemetry currently is attempting to support a dizzying number of languages and frameworks. OpenTelemetry is a g iant project. It spans dozens of languages, hundreds of libraries, and countless backends. To keep things sane, the project splits work into two buckets: There exists the otel-collector, the thing that runs along the thing so that you can ship logs metrics and traces. That copies the same rough pattern. But for the languages when we're talking about core vs contrib this is what we're talking about. Stuff that breaks goes in contrib, stuff that doesn't break goes into core. Now the reason this causes a conflict. is massive overkill for most projects. You don't want 300 exporters to add the one you typically need. On the language side, this isn't that big of a problem. gives you the stuff you need for flask. However on the collector side you end up having to do the OpenTelemetry Collector Builder to make your own collector (or just kinda ride the wave and hope it works out). While cool that this exists, it's a lot of scope to ask a team to take on. So I believe I have captured the workflow of adding a new feature to OTel. You can check my homework here: Things I'm not really clear on So because OpenTelemetry is a CNCF project, I figured it made the most sense to compare them to other CNCF projects. My basis for comparison is Envoy and Prometheus. I have used a hacky Python script I've used before for measuring the "health" of open-source projects, which is probably not the best. However I'll include a link to the raw data without the charts so folks can review it and (more than likely) find a problem in what I generated. So we look at 24 months of activity for Envoy and what we see is a pretty healthy project. There's good distribution of authors, mergers, issue closers. is obviously pretty important to the project but in general there's a good bench of people to step in if needed. I've attempted to filter out all the known bot traffic. Let's compare that to one of the OpenTelemetry languages. The ones I have the most professional experience with are Golang and Python, but I hear from a lot of folks in the community that the Ruby and PHP ones struggle a lot. This is the PHP one for the same period. So we see pretty clearly that there's way too much concentrated on 2 people. This is not a healthy open-source project and they clearly don't have enough people to cover the kind of scope OTel needs to cover. Same story with Ruby. In comparison the "strongest" OpenTelemetry SDKs in my opinion, Golang and Dotnet (although Python is also no slouch) look more healthy. So the first issue is maybe the least surprising. There's too much concentration among too few maintainers. Your authors shouldn't also be your mergers and your issue closers. Ideally these tasks should be distributed out more evenly. For what its worth I think the maintainers have done a good job of attempting to keep their discussions public. It was very easy for me to find the public meeting notes of the different groups of maintainers, read through them and see what was going on. I don't get the sense that these maintainers are trying to stop people from getting involved as much as the expectations of stability have, more or less, frozen the project in place. The issue is more a classic case of "someone has to pay the maintainers". The project is too complex for someone to realistically do this as a hobby. I think any project signing on for such long stability contracts cannot turn to the community of hobbyists expecting assistance. I can't join calls and do the things I would be expected to do for a project of this size and importance for free. But it also means that the people doing this critical work have expectations placed on them by their parent organizations. So these SDKs have too few maintainers. But that doesn't fully explain why it seems to take so long for new features to get through the stack. My guess for that was that somewhere in the process between submission of the new idea and the formalization of the idea was a long discussion that took a million years. So with this level of surface area across different frameworks and languages, it makes sense to concentrate the conversation about conventions in one place. That lives here: https://github.com/open-telemetry/semantic-conventions If vendor debate is causing the slowdown, we should (in theory) see this slowdown in PRs here. Then you should see the slowdown basically propagate out. Spoiler alert, I was wrong about this. Big thanks to the OpenTelemetry people for having good conventions on labeling their PRs which made this much easier. So if is the slowdown, let's look at the slowest PRs there. Yeah some of them are pretty slow, but there are some complex topics being discussed. However interestingly this slowdown doesn't really trickle into the SDK/API space, suggesting that OpenTelemetry is going a good job of keeping these conversations siloed off. If we look at Python we see that their slowest PRs aren't related. In reality the slowdown for these are the extra required check imposed by the which requires another maintainer. But that seems appropriate and takes us back to the initial problem of "not enough maintainers". So after looking at all of this, the pattern becomes clear. A new feature takes a very long time to make it to the end user in OpenTelemetry because they take stability very seriously, combined with a relatively limited bench of talent to pull from. Once things make it through the entire stack, implementing the API and getting that API change through to the end user falls on an overworked maintainer pool. So what do we do? I think one idea worth exploring is adding some sort of time-bound beta tier. Basically between the "Experimental" and the "Stable" in the following diagram. The problem is that for end users, due to the extra steps to use Experimental features, they might as well not exist. 99% of us have no idea when an experimental feature is added and we would never engage with it. But if I knew the feature would stick around for at least 12 months without a removal and was more accessible to me as an end user, it could actually help the project get more actionable feedback. Basically a feature would go Experimental (pretty low usage) -> Beta (more exposed to the end user than Experimental) -> 12 months -> Removal or Stable. Now confusingly Beta exists for Otel but is used for SDKs, not for components. Like Rust is a Beta but it seems like Profiles cannot be a Beta. Honestly it's nearly impossible for me to figure out like what labels should apply to what things. I suspect nobody really knows. Here's the explanation of Beta that I think only applies to SDKs. In addition it is, respectfully, misleading to imply that Go and Ruby are being maintained at the same standard. This isn't a shot at the Ruby folks — they are doing heroic work with what they have. But pretending parity exists when it doesn't just creates confusion and quiet resentment when a user shows up expecting one experience and gets another. Being honest about maintenance tiers would let people make informed choices and might attract more help to the other tiers by naming the problem out loud. Finally I would try to surface these problems more openly for OpenTelemetry from the perspective of "we need more maintainers". I feel like the people doing this work probably knew there was a problem, but it seems like the community at large has no idea that there is a need for frankly more engaged ideally independent maintainers and contributors. OpenTelemetry is a great project that is doing great work. It's doing, frankly, heroic work at this scale with this few people. But I think in order to actually replace the vendor specific SDKs we need to start getting a bit more pragmatic about what is realistic to do in terms of stability contracts and number of languages. I don't think breaking changes are as devastating to the community as these promises imply as long as they are communicated well and I think with this thin of a bench of maintainers, something has to give. Anyway feel free to check my data for accuracy and let me know if you find problems! Core → Maintained directly by the OTel project. Small, stable, vendor-neutral, and tightly reviewed. This is the "spec-defining" surface. Contrib → Community- and vendor-contributed. Broader, faster-moving, and covers the long tail of integrations. OpenTelemetry Enhancement Proposal (OTEP) ( https://github.com/open-telemetry/opentelemetry-specification/tree/main/oteps/ ) Once the OTEP is accepted, the text goes into the Specification directory in the same repo. After that it seems to go to Semantic conventions. This seems to be where we get down to the specific details and where most of the long discussions seem to live. At this point we're talking about more or less a permanent commitment to this design and where the lock-in process becomes very hard to change. Each of the SDKs implements the API surface that is defined in the specification. Now some of the SDKs have done 2.0 breaking changes, so it does seem like the earlier "please no 2.0 at all costs" sentiment has been abandoned (which I think is smart and good). Contrib / instrumentation. This is slightly more mushy. Looks like they should track latest API/SDK but each contrib package may version independently so its more flexible as a design. Collector + OTLP. The data has to actually go somewhere. OTLP (wire protocol) has its own stability lifecycle and specification ( here ). Collector components have their own stability in their READMEs and as far as I can tell that's kinda all over the place. It's unclear how long the OTEP -> Specification process takes. I've looked through the Git history but there doesn't seem to be any predictable number or cycle. I don't fully understand what is the relationship between all these stability commitments. Does Collector + OTLP group work in lockstep? Can a language "fall out of scope" if you lag too far behind?

0 views
Anton Zhiyanov 3 weeks ago

Relying on Go

Everyone is creating a new programming language these days, often one that's "like Go but with more features" or "like Rust but simpler". Solod , a systems language for C and Go developers, might look like one of those languages, but it takes a different approach. Solod is not "Go-like" in the usual sense, nor is it an attempt to "fix Go's mistakes". At the language level, Solod is literally a subset of Go. Solod reuses much of Go's existing tooling, including syntax highlighting, LSP, linters, and the package management system. Take this quick-start guide, for example: Quick start Install the So command line tool: Create a new Go project and add the Solod dependency to use the So standard library: Write regular Go code, but use Solod packages instead of the standard Go packages: Run without saving the binary: There's nothing new here. It's mostly standard Go workflow, except for , which is a Go program that mimics . Solod also reuses a lot of Go's standard library code and tests. Some of it is taken verbatim from Go's source code, like these two string functions: Of course, Solod retains the Go authors' copyright. Some code requires changes to support the manual memory management with explicit allocators used by Solod: You can probably see the resemblance. Go tools don't know that Solod is a subset of the full Go language, so they won't flag features Solod doesn't support, like function literals or iterators. These diagnostics come from the custom tooling: Also, although a substantial part of Go's standard library is ported verbatim or with minimal changes from the original source, that doesn't mean the code is automatically correct. Solod still needs its own tests, including ones that run under sanitizers and static analyzers. All Solod code is translated to regular C11 and then compiled with GCC or Clang. Solod therefore relies on C tooling and decades of optimization work just as much as on Go's. Solod code: Translated C code: The C version is noisier, of course, especially for more complex programs than this one. But it remains readable. And since there's no runtime, interoperability between Solod and C costs nothing. A new language doesn't necessarily need a new ecosystem. Solod relies heavily on Go, and I see that as a strength, not a weakness. Reusing Go's proven tools and standard library makes Solod more reliable and easier to work with. If you're interested, take a look at Solod's readme — it has everything you need to get started. Or try it online without installing anything.

0 views
Chris Coyier 3 weeks ago

Outburst

Just had a little trip up to my favorite city of Juneau, Alaska. So lovely. Here’s just one story from the trip. There is a Big Thing going on there right now which is essentially an impending flood that is going to happen any day now. It’s an “Outburst Flood” or GLOF (“glacial lake outburst flood”). The weather.gov page explains in one graphic: So there is the famous Mendenhall glacier. Mendenhall lake, formed by it’s melt, is like 30 minutes from downtown Juneau, so it’s quite accessible for even cruise ship visitors to go get a look. Suicide Basin fills up with water during the warm months from glacial melt, and the Mendenhall glacier acts as an ice wall between it and Mendenhall lake. But at some point, the basin gets so full, the water from it starts flowing underneath/through the glacier into Mendenhall lake. There is a river from Mendenhall lake out to the ocean. When Mendenhall lake starts rising from the basin runoff, it goes out that river, and that river rages for a good couple of days. Hence the flooding. That river? Half the people in Juneau live in the Mendenhall Valley where that river runs directly through. Now it’s not all under evacuation flood watch, but some of it certainly is. And it’s rather unknown how bad any given year is going to be. It’s been very bad: My friend Justin lives in the Mendenhall Valley, not far from the river, and when it goes, he can loudly hear the river raging. As I type, we’re just days away from the prediction of August 8-12 from the basin draining. Justin and I climbed up Thunder Mountain (oof, it was actually 8.5 miles and 3,800 ft) and from the summit had a great look down at the entire valley, seeing the whole river area at once. Driving around there on roads by the river, you can see how the city has put up huge wall embankment things to hopefully stop the worst of the damage: Seems like it’s fairly unknown if it’s really going to work. If you’re interested, KTOO did a mini podcast on it last year getting into some details. Like one of the proposed solutions are literally bombing the glacier 🤔. Apparently now it’s leaning more toward the Army Corps of Engineers digging a drainage hole of sorts, but I don’t think any of it is totally sorted out yet.

0 views
Xe Iaso 3 weeks ago

SigV4 authentication is surprisingly complicated

SigV4 looks simple: sign a request, check the signature. Then you implement canonicalization, clock skew, and a cache that isn't allowed to hold your key. Tigris is a drop-in replacement for AWS S3 (or GCS, anything S3API compatible). As such, we need to be fully compatible with both the mechanisms and semantics of S3 including the SigV4 authentication protocol . This is the lingua franca of authentication in the object storage landscape; even Google Cloud Storage has a way to enable SigV4 support so you can use existing applications against its object storage service. At first I thought that SigV4 was fairly simple. Clients sign requests, servers do the same work and make sure the result matches. The main sticking point is that the cryptography involved is symmetric cryptography, the kind where both parties need to have the same secrets. This makes some scaling issues weird, but we'll get into that in the future. Note This is only going to be talking about authentication (ensuring the identity of a remote client), not authorization (ensuring the client has the permission to do something). Authorization will come in the future for reasons that will become obvious when you see that post. We basically needed to implement a compiler. That is not a typo. At a high level when a client signs a request with SigV4 you get an access key ID and secret access key. The access key ID is functionally a username and the secret access key is functionally a password. Admins can identify keypairs by the access key ID (without special training or tools) and services use the owner of the access key or policies delegated to that access key to determine what actions that client may take. SigV4 uses HMAC (hash-based Message Authentication Code) and SHA-256 (SHA-2 with a 256 bit hash width) to do authentication by creating salted hashes based on request metadata. In order to send a SigV4 request, clients take the outgoing request, reduce it to a canonicalized form, and sign it with a symmetric key derived from the secret access key, the current date, region of the service, and service name, kinda like this Go code: As an example, let's see what a signed request to a HTTP debugging endpoint looks like on the wire with and without the signature: And when you add the signature with : Note This is not a live keypair, it was specifically crafted for this post. Breaking it down we have two extra headers in the request: On the wire, HTTP/1.1 requests look kinda like this: However the headers could be sent in any order, and changing the order of request headers doesn't result in different requests. Additionally any query string parameters could be formatted in any way a client (or server) could imagine, including the use of semicolons to separate values . All attempts to canonicalise HTTP requests MUST deal with this ambiguity and define their own rules. SigV4 canonical requests are made up of a few parts: For that example request, the canonical form would look like this: As the request has no body, the empty sha256 checksum is put as the body checksum. Note This exact approach requires clients and services to buffer the entire request body before processing it. There is a subset of SigV4 that supports arbitrary-sized bodies without having to buffer the entire request using , which requires extra logic that is way out of scope for now. If you want to learn more, give your favourite AI agent the following prompt: Additionally, when you are doing presigned URL uploads in object storage, you replace the body hash with the fixed string when canonicalizing because you have no way of knowing what data the client will upload or what the SHA256 checksum will be. To make the signature, you take the sha256 checksum of the canonical request and then HMAC it against that derived signing key: And construct the header based on your access key ID, service region, and service name: AWS has made an extension to SigV4 that uses asymmetric cryptography called SigV4a (the "a" means asymmetric). Instead of using symmetric cryptography on both the client and server in ways that means the server needs to either know the client's secret access key (or a value derived from the secret access key), SigV4a uses key derivation functions to derive a cryptographic keypair. Servers authenticating requests fetch the public key from IAM. Only the client and IAM know what the private key is, and that private key is what signs outgoing requests. I'd love to use SigV4a more because it makes adding additional services to the mix (such as a git service ) a lot safer as you can have those additional services exist in different trust domains than the core product. This is the core of how microservices end up happening. However, it's not super widely used even within AWS. The only SigV4a use I can find in Amazon is S3 Express Zones , however they may end up using it in other services I'm just not aware of. When I did my own experimentation with SigV4a (where I was implementing my own IAM server so that I really understood this all at a low level), I had to copy a lot of internal AWS SDK code into my repo in order to get it working. I'll talk about SigV4a some more another time. One of the weaknesses of using signatures for API authentication like this is the problem of replay attacks. When you make a naïve signature of a value, there's no real way to tell when that signature was created. If you sign a request to create a compute instance at time instance t0, it's still technically valid at any other time instance tN. This is why the canonical form of SigV4 requests includes the current date and time: This means that the request was signed on July 15, 2026 at 20:54:32 UTC. Time changes constantly (at least at the rate of one second per second!) and the client has to have a working clock in order for TLS to work. Servers can trivially read the contents of and reject old requests. This means that you don't need to add or store nonce (number used once) values with each request because that doesn't scale . Note A lot of the security of this authentication protocol is predicated on TLS being used to encrypt the authentication headers over the wire. If TLS is not in use or is compromised by administrative policy, you're probably in a very weird exceptional situation that is very wrong in the first place. An easy example is an enterprise network with endpoint manglement software that does deep inspection of every user action. As a side effect of this, you need to set a temporal skew window for validating requests. This window needs to be generous enough to accommodate slow clients, sloppy timekeeping on the client side, highly latent clients, leap seconds , or other exceptional temporal phenomena. In general time synchronization is a surprisingly hard problem , so it's best to just be tolerant of clients in order to make things more robust in practice. AWS uses a temporal skew window of 15 minutes for validating requests. I'm going to use a window of 5 minutes for my API because 300 seconds is a nice round number and I don't have to deal with the same amount of legacy code that AWS does. So all of this SigV4 business had been working really well for Tigris. Then we worked with a few customers who needed a local cache to fully saturate their hungry GPUs. To be fair, Tigris is plenty fast, but the real thing that kills AI training is latency and something that runs locally will always be faster than the cloud. In order to provide that sweet middle spot between making everything rely on the cloud and having everything local, we made TAG , the Tigris Acceleration Gateway. This effectively gives you most of a Tigris region in your own infrastructure. When you connect to TAG, your code uses its existing access keypairs, buckets, and code. You point your code to TAG, you point TAG to Tigris, and then everything is cached for you. But how does TAG authenticate with your code? TAG doesn't have access to all your existing API keys (and to be honest it shouldn't), but it's still able to authenticate them with SigV4 authentication. TAG and the IAM server both implement a signing key proxying feature that lets a client and TAG both prove their identity to Tigris. Once that proof is sent, then TAG gets the intermediate derived signing key and uses that for locally validating requests, kinda like this: The actual implementation in TAG involves some derived AES logic so that the derived signing keys are very much limited to the client that requested it (namely: the AES key is the SHA256 encoded form of the proxy secret access key). One of the weird parts is that the canonical form of the proxied requests differ from the normal SigV4 canonicalization process, namely looking like this: This is signed using the same SigV4 signature process as before but added differently to the request: And then TAG reads the response from Tigris, caches those derived signing keys, and then uses those in the standard SigV4 process to authenticate clients: no round trip to the cloud required. The happy path is exactly what I thought it was. Reduce a request to a canonical form, run four HMACs, compare the result. That part fits in an afternoon. Everything expensive lives in the questions around it. Which bytes count as the request? Whose clock decides that a signature is still good? Who gets to hold the key that proves any of it? Each question has an obvious answer, and each obvious answer is wrong in some specific way you only find by implementing it. That last question is the one that surprised me. I read symmetric cryptography as a hard limit: if the verifier needs your secret, the verifier has to be Tigris. It isn't. SigV4 derives its signing key through a chain of four HMACs, each one scoped tighter than the last: date, then region, then service. Those intermediate values can travel without the secret behind them. TAG rides that. The key it holds stops working when the UTC date rolls over. It covers one region and one service. You can't walk it backwards into a secret access key. We also didn't write any of this, which is its own kind of relief. SigV4 is old, widely deployed, and hammered on by every S3 client in existence. Any compatibility bugs here are ours. The protocol's bugs are everyone's. The place a protocol bends is usually some intermediate value that somebody already designed to be thrown away. If you want a Tigris region in your own datacentre, the Tigris Acceleration Gateway caches your buckets locally and authenticates your existing keypairs with the same SigV4 dance your SDK already speaks. : The fixed string to signal to the server which authentication mechanism is in use. The rest of the string is information about the request signature so the server can properly canonicalize the request. : The date and time (UTC) of the request so the server knows when the request was signed. Servers will use this request date in order to reject old requests to prevent replay attacks . The HTTP method ( , , , , etc.) The URI path of the request ( , etc.) The sorted canonical query string (you must exactly match the server-side canonicalization logic) The signed headers terminated with two newlines The sorted list of signed headers joined by semicolons The SHA256 checksum of the request body : the HTTP Host of client requests (EG: tag.default.svc.cluster.local) : the Tigris keypair used to authenticate TAG itself (must be in the same organization as the client) : the time of the request in unix timestamp format : the hex output of signing the canonical form of the request against TAG's secret access key

0 views
Anton Zhiyanov 3 weeks ago

Going Backward

Go's standard library has a package with a function called . It lets you iterate over the elements of a slice in reverse order: If you're not deeply familiar with generics and iterators, the natural reaction to this signature (and to the others in the package) is: "couldn't this have been made simpler somehow?" To answer that, let's run a thought experiment. Let's picture ourselves as a distant ancestor, living in the pre-iterator era, who decided to implement from scratch. Our imaginary ancestor doesn't work at Google, so don't project their decisions onto the Go development team. They had their own reasons — and no Jira. A pleasant, sunny summer day, birds singing. You're at the keyboard as usual, and suddenly you decide to write a function for walking a slice in reverse order. Anything beats working on yet another Jira ticket. Usage example: The implementation is simple and works reliably. There's one drawback, though: creates a copy of the slice, which can be wasteful for large slices. Besides, the sun has hidden behind a cloud, and it looks like rain is coming. You decide to work a bit more. To avoid copying the slice, you decide to return a closure that knows the current position in the original slice and returns the next element on each call: Usage example: Now it allocates O(1) memory instead of O(n). That's better. Before moving on, you glance out of the window. Yep, sure enough, the rain has started, and the sky is even cloudier than before. Excellent working weather! Something about the calling code keeps bothering you. It came out rather imperative. You'd like to hand the loop mechanics over to and leave the caller with nothing but the application logic (whatever it is you do with the slice elements). You decide to complicate 's signature a little. Now it will return an iterator function that takes a callback as an argument and applies it to each element of the slice: The function returns a — that's so the callback can signal when it wants to stop the traversal early. Now you can turn the loop body in the calling code into a callback, and you don't need the loop anymore: Mmm, very functional. One small nuance: 's signature looks a bit heavy. You add a separate type for the return value: The function looks much better now: Praising yourself for inventing the iterator, you walk over to the window. It looks like the weather's gotten worse. The rain is coming down in buckets, and the sky is so overcast that it's grown as dark as evening. It's all great, but then it hits you: an ordinary over a slice returns both the index and the element's value. Your iterator returns only the value. You decide to fix this vexing oversight: Usage example: Since the result's signature has changed, it no longer fits the type. What can you do — you'll have to add a new type. After ten minutes of deliberation, you decide to call it : You get up to stretch your legs, and go to the window. The downpour is so heavy you can't make anything out. Lightning is flashing. Hail the size of your fist is falling — you've never seen anything like it in your life. Well, these things happen! Have you thought of everything? Seems so. But you're not going back to Jira tickets just yet. Refreshing your memory of the Go spec, you realize that besides ordinary slices there are "user-defined" ones — types whose underlying type is a slice: works perfectly well with — the compiler accepts a value of type since its underlying type is : But what about this? Here's where the difference between and shows up. When you assign the function itself, it's the signatures that get compared: versus . Signatures match only if the parameter types are identical. But and are different, even though one is based on the other. The signatures differ → you get an error. Scratching your head, you turn to the spec once again and find a special generic syntax: . It represents the set of all types whose underlying type is . Just what you need! Now you'll have to parameterize not only the element type ( ) but the slice type ( ) as well. is needed for the returned values, while lets the function accept not just , but any types based on it: Now the example: It works! You've ended up with something similar to from the package. You exhale wearily and walk over to the window. The downpour and hail have given way to a hurricane. Trees and billboards go flying past. Toads, for some reason, are falling from the sky. To take your mind off the strange events outside the window, you keep pondering. An ordinary is already great. But it would be even better if the traversal logic itself were configurable. On the other hand, if you end up with a lot of parameters, a strategy would suit better. And, by the way, it wouldn't hurt to add a factory that produces iterator factories according to given criteria... Before you can finish the thought, the ground outside the window tears open with a deafening roar. An enormous black hand, streaming molten lava and flickering flames, bursts out of the fissure, seizes you, and drags you straight down to hell. P.S. Despite the article's tongue-in-cheek tone, the "complicated" version in the standard library is justified ( just follows suite with other package functions). But if you're doing something similar in a project that solves a specific problem — it might make sense to stop at the simpler option.

0 views
Anton Zhiyanov 1 months ago

Solod 0.3: Concurrency, JSON, more safety

Solod ( So ) is a subset of Go that translates to regular C — with zero runtime, manual memory management, and source-level interop. It's designed for two main audiences: At the end of the v0.2 post , I said the obvious goal for the next release was concurrency, along with the stdlib packages that support it. That's what v0.3 is about. So now has threads, channels, worker pools, mutexes, and atomics — enough tools for parallel data processing or handling network connections. This release also adds a streaming JSON package, a bunch of safety checks (escape analysis, leak checking, nil-pointer panics, stack traces), and proper and commands. Threads • Channels • Worker pools • Sharing state • JSON • Safety net • Tooling • Wrapping up The new package is the foundation. It provides real OS threads, backed by pthreads. If you're familiar with Go's goroutines, the code will look similar — but there are some important differences. Solod doesn't support closures, so takes a function and an argument, instead of just a like you'd expect in Go. Other important differences: starting an OS thread isn't free, and you always have to on it (or it), or it will leak. That makes a good fit for a small, fixed number of long-lived threads — but not for thousands of short-lived tasks. For those cases, it's better to use a pool (shown below). Threads in Solod communicate with each other through channels, like goroutines in Go. A channel carries values of a specific type. By default, sending or receiving on a channel blocks until both sides are ready, so a channel also works as a synchronization point. A couple of So-specific moments here. When you create a channel, you give it an allocator ( in this case), and you call when you're done with it. Also, writes to a pointer you pass in, instead of returning the value directly. It returns a , which is when the channel is closed and empty. So, a typical loop in Go becomes in Solod. Allocators are a key concept in Solod. The language doesn't allow hidden heap allocations, so any function that needs to allocate memory must take an allocator (the interface) as its first argument. Buffered channels can hold a limited number of values without having a receiver ready — just pass a non-zero size with . If you don't want to block forever, use or with a duration. They return or instead of getting stuck. Threads are expensive, so spawning one per task doesn't scale. For handling many short-lived tasks, use : it uses a fixed number of worker threads that take tasks from a queue. works similar to Go's — it blocks until all submitted jobs are finished. This program takes about 200 ms to run (even though there's 800 ms of total work), because 4 workers run concurrently. You might think OS threads are much slower than Go's goroutines, but for pools, that's not the case. On realistic workloads, is usually only about 10% slower than Go, whether the tasks are CPU-bound or waiting on I/O. Channels are a different story: handing off work between threads requires a kernel wakeup, while Go does this in user space, so it can be several times slower. Check out Go-flavored concurrency in C for more details. One way to share state in Solod is by using channels to communicate it. However, sometimes you just need a shared counter or a lock. For that, the new release introduces the and packages. Here's an example of an atomic counter being updated by 50 tasks running on 4 threads: A regular incremented with would cause a data race and give a different result each time. Here, the result is exactly 50,000 on every run, thanks to the atomic counter. The package provides , , , and types, all of which are lock-free and safe for concurrent use. For anything more complex than a counter, use , which provides , (a condition variable), and (runs a function exactly once). One thing to watch out for: unlike Go, a So mutex's zero value isn't ready to use — you need to it before locking and it when done. Go's relies on reflection to marshal arbitrary structs. Solod has no reflection, and uses a different approach: a token-level API. You read and write one JSON token at a time, and the and types take care of the syntax — adding commas and colons, checking UTF-8, and rejecting bad input. Encoding is done through a series of calls that match the structure of your document: Decoding pulls one validated token at a time with . You can check each token with and read its value using typed getters like , , or : This is a simplified example that works only because the decoder doesn't allocate any memory. In a real-world situation, you'd need to use an allocator . The decoder works the same way whether you're using an in-memory document ( ) or reading from a stream with an ( ). This means you can decode data directly from a source without having to buffer the entire message first. Both the encoder and decoder use minimal memory and will reject invalid JSON or non-UTF-8 strings. As you can see, API is low-level and not nearly as ergonomic as it is in Go, especially when it comes to decoding. But on the bright side, it's 10 times faster and almost doesn't allocate, unlike in Go. Solod compiles to plain C, which is fast but not very forgiving: if you use an out-of-bounds index, dereference nil, or divide by zero, you get undefined behavior that could crash the program or silently give wrong results. The new release addresses some of these issues. Escape analysis . Returning a pointer to a stack-allocated value is a classic C footgun. So now catches the common cases at compile time: While the escape analyzer doesn't catch every case, it's still quite useful in practice. I actually found a couple of dangling pointers in the standard library code with it, even though I was sure there weren't any. Leak detection . Solod has no garbage collector, so a forgotten is a real memory leak. helps catch these leaks: it wraps an allocator and keeps track of every allocation and free that goes through it. This way, you can monitor the program's memory usage in real time instead of guessing. Wrap once, allocate memory through the tracker, and have a background thread log the stats at regular intervals: The tracker is lock-free and only uses a few atomic operations for each allocation, so it's cheap enough to keep enabled in production. Nil-pointer panics . If you try to dereference a nil pointer, it will cause a panic at runtime instead of a raw segmentation fault: Stack traces . When a program panics, the flag controls what happens next: Stack trace frames represent each function in the call chain: The same system handles assertions like slice bounds, index-out-of-range, , and similar checks. Instead of calling C's , they panic in a way that respects the flag. There's also a new flag that enables C sanitizers ( and by default) to help you catch more issues during development: 'so test' and 'so bench' . Solod now has built-in test and benchmark runners. finds functions in a package's subdirectory, creates a runner, transpiles it, and runs it. does the same for . A typical package layout with tests and benchmarks looks like this: There's also a quick check for memory leaks: gives you a tracking allocator (described in the 'Safety net' section above), and the test will fail if anything allocated with it isn't freed by the end of the test. Fuzzing . Since Solod is a strict subset of Go, any So package is also a valid Go package. This means you get Go's built-in fuzzer for free, making fuzz testing pretty easy. So's package takes advantage of this by using Go's own as an oracle, making sure that every JSON document accepted by So is also accepted by Go. Automatic linking . The new directive lets a package specify which C library it needs, and gathers these libraries and passes them to the C compiler. The standard packages already use the new directive, so importing links with , and or links with — you no longer have to set manually. With v0.3, Solod reaches an important milestone: a program can now do multiple things at once. The JSON package gives programs a standard way to communicate, and the safety checks help prevent silent failures — both during development and in production. There's still a lot to do, of course. In the next release, the standard library will keep growing, and the language and tooling will get better to make programming in So more convenient and safe. If you're interested, take a look at So's readme — it has everything you need to get started. Or try So online without installing anything. Go developers who want low-level control without having to learn another language. C developers who like Go's style.

0 views
Unsung 1 months ago

“Creativity is fundamentally not an efficiency problem.”

A computer science professor Paul Cantrell, on Mastodon : Creative work keeps taking roughly the same amount of human labor / attention / care, even as new technologies accelerate or remove things that used to take time. This is because creativity is fundamentally not an efficiency problem; process is not just the means of producing output, but rather a labor vessel that holds the near-invisible work that is truly important. One can feel the care that goes into creative work without being aware of that work, or even being aware that work of that type exists at all. This feeling is approximate, loose, vague, but cumulative and eventually all-important; work with no care behind it wears thin and tends to fade as people live with it over time. This really resonated with me. Elsewhere, Ginger Bill, in a recent – meandering, but thought-provoking – essay titled “Good tools are invisible” : I constantly see some people praise it not for what actually makes it good, but by taking the things it’s bad at and turning them into a puzzle to have “fun” solving. I’ve had people tell me how “fun” it was to build a macro to handle some one-off text-refactoring problem. But when I looked at what they were doing and how long it took, my honest reaction was: I could have done that in Sublime in a minute with multiple cursors, or just written a quick script. […] That’s what I mean by “invisible tools”. When you’re proficient with your editor of choice—whatever it is—it disappears into the background. But the moment it cannot handle something easily, it stops being invisible. What baffles me is that so many people treat that friction—the effort of working around a tool’s limitations—as the “fun” part, and then advertise it as evidence that the tool is great. […] The text-editor-macro anecdote I mentioned is really about a gap between feeling productive versus being productive . There’s a sensation of cleverness that comes from solving a fiddly problem, and it’s easy to mistake that feeling for actual output. A tool that makes hard things feel heroic and clever feel like an achievement can register as “powerful” while quietly being slow. The honest test isn’t how engaged or clever you felt, it’s wall-clock time and how many mistakes you made getting there. This I had more of a mixed reaction to. I think it’s necessary to expect from tools to get out of the way, but there’s also nothing wrong with having fun with them. My simple go-to example is this: When writing code, I sometimes use Find & Replace All, and am done within a few keystrokes. But sometimes, I press Find and then replace one at a time, jumping methodically through the file, and seeing each string in situ before changing it. I know the tool could do it all for me. I know I could be more efficient. But this intentional slowing down allows me to refamiliarize myself with the code, visit its forgotten nooks and crannies, and make sure I understand where and how the thing I’m changing is actually used. The editor I use allows me to not be efficient when I choose not to be. In my work, flow operates at different speeds; a good tool understands that and doesn’t force me into a particular one. I think ultimately indeed, the tool does need to disappear, and make you be in charge of whatever speed you want to operate at, and how much friction or difficulty you choose to face (do you bump the lamp or not?). But it’s not as simple as always “reducing wall-clock time and mistakes.” Like Cantrell says above: Creativity is fundamentally not an efficiency problem. #ai #craft #flow #toolmaking

0 views
Simon Willison 1 months ago

A Fireside Chat with Cat and Thariq from the Claude Code team

Earlier this month I hosted a fireside chat session at the AI Engineer World's Fair with Cat Wu and Thariq Shihipar from Anthropic's Claude Code team. We talked about Claude Code, Claude Tag, Fable, coding agent security, evals, tool design, and how Anthropic use these tools themselves. The full video of the session is now available on YouTube . Below is an edited copy of the transcript, with extra links and my own bolded highlights. A few top-level notes if you don't want to watch the video or wade through the whole transcript: Simon: Claude Code came out in February of last year — it's under a year and a half old, and it was originally just a bullet point on the Claude Sonnet 3.7 launch . How has what you do on a day-to-day basis changed in the past year , now that we have these coding agents that actually work for us? Cat: I remember when we first came out with Claude Code and Sonnet 3.7, you would give it a task and you would have to closely monitor every single little thing it tried to do. I would read every permission prompt extremely carefully. I would frequently say no — no, no, no, did you check this file? Did you check that file? And now it's been incredible with every model generation. I feel like we've all gotten a chance to take a step back and delegate a lot more of the menial implementation to Claude . It's freed up a lot of our time to think about more creative work, like: what is the right experience that we should be providing to our users, now that we know Claude Code can implement a lot of it? And now with Fable it's a totally different step change improvement. We see for a lot of our use cases that you can actually one-shot a ton of features with Fable now . Thariq: I remember the first text I got about Claude Code. One of my best friends was like, "You need to go try Claude Code." It was about when Opus 4 came out, and I tried it and I was like, "Oh, shit. I need to work at Anthropic now." And that was Opus 4 — great model, but you were reading permission prompts. It's kind of crazy how much amnesia we have, where I'm like, oh, auto mode has always been here, right? I don't even remember pressing yes and allow. For me, the big thing I'm trying to push myself on is that we have to do higher quality work than we've ever done before . The outputs are incredibly high quality. I've been using it to edit videos a bunch , and I'm like, okay, it has to meet the very exacting demands of our brand team in a couple of hours or we just can't do it. That's how I'm trying to shift with Fable: the best work we've ever done, faster than we've ever done it before . Simon: What's a piece of conventional software engineering that was true a year ago that you don't think holds anymore in this new world? Cat: One of the biggest shifts we're seeing in the eng skill set: two years ago it was pretty typical for a product manager to go talk to a bunch of customers, align over the course of six months with cross-functional teams on some PRD, and write a thorough spec on exactly how we'll implement this before the first line of code gets written. Now things are completely turned the opposite way. For a lot of engineers, the push I would give to folks in the room is to develop more of your business sense and product sense on what it is we should build , because the timeline between having an idea and building it is so much shorter — it's down from six to twelve months to maybe even a week. That means all of us need to have better taste on what is worth building, what will actually inflect the businesses we're working on. So it's an increase in value on product taste and business sense , and a bit lower on execution in most product domains. Of course, for infra there's still a very heavy emphasis on making sure all the details are right. Thariq: For me, it's that rewrites are now good . Simon: The worst thing you could do is now actually fine! Thariq: Exactly. All the Mythical Man-Month stuff — never rewrite — I'm pro-rewriting now. If you have a good test suite — and I think the rewrite actually forces you to make sure you have a good test suite — but I think what people undercount is that a codebase is a spec, and maybe it's the only copy of the spec that you have , because no one knows every branching part of the codebase. You can take this as an artifact and distill it or create other versions of it. We rewrote Bun in Rust and it works great — it's live for me right now. Simon: You're not shipping Claude Code on Bun-in-Rust yet, right? Thariq: Internally we have. (Actually it looks like Anthropic started shipping Claude Code on Bun-in-Rust to everyone on June 17th .) Simon: The other big launch recently was Claude Tag — that's what, a week old now, at least for the rest of us. I understand it's being used at Anthropic by non-engineers a great deal. What kind of things are non-engineers doing with Claude Tag? Cat: Claude Tag is a Claude that lives in your team's collaboration tools. We launched it last week within Slack. The thing that's different about Claude Tag is it's multiplayer by default . Once you add Claude Tag to a Slack channel, you can chime in, your teammates can chime in, and you can collaborate together on the PR. The other big difference is that it's proactive instead of reactive. You can tell Claude Tag, "Hey, monitor every bug report in this channel, put up a PR to fix it, and tag the engineer who last touched this part of the codebase," and it'll do it for the lifetime of the channel without you having to manually tag it in. And the third big shift is that we've added team memory into this . If you tell Claude Tag your preferences in the channel, it'll remember them for every future post. If you always want it to debug outages but you don't want it to debug warnings, just tell it that in natural language in the channel and it'll remember it for you and everyone else on your team. Internally, we see Claude Tag as the evolution of Claude Code. We see this as a large shift in how we work internally. Claude Tag currently lands 65% of our product eng PRs. Simon: For all of Anthropic, or just for Claude Code? Cat: This is just for our product engineering team — our internal version of Claude Tag lands 65% of our product PRs right now . And this is a huge shift; this is more than 50% of our PRs. The way we see people split work between Claude Code and Claude Tag is: Claude Code is still the best place for your most complex tasks, when you're interactively iterating with the agent. But Claude Tag is great for having it work proactively on your behalf , so you no longer need to manually kick off Claude Code for all the bug reports that come up for features you're working on. Thariq: And for non-coding cases: for example, before this talk we asked Claude Tag, "Hey, when is Fable releasing?" We wanted to make sure we'd line it up with the announcement. Claude Tag would search our Slack and look at who's been saying what. As a search engine for your company, it's really valuable. It has all the context for your product, so you can ask it metrics-related questions — often when you're making decisions you want them informed by what the metrics say, so you hook it up to your event store. I've seen our marketing team do things like, "Hey, tell me about this feature." They're not programmers, but Claude is a programmer — it can clone the codebase and say, "This is the feature, this is what it looks like, this is a recording of me using the feature ." It enables a whole wide variety of things, and I think we're still early in figuring that out. Simon: One of the problems I've had with coding agents is that I get how to use them as an individual, but I'm not really clear on how to use them in a team environment. It sounds like Claude Tag is your current answer to that team collaborative layer for this stuff. Cat: Exactly. And a large percentage of our sessions are actually multiplayer right now. Maybe I say, "Hey, I think we should implement this new feature in Cowork," and I'll tag in Claude Tag to do a first pass at it. Then I'll tell Claude Tag, "Share a recording of your final implementation," and I'll tag in design to take a look. They'll nudge it, then pass it on to eng to take it to the finish line and get it out to prod. It's been this very fluid experience. We're still trying to iron out what the social dynamics are for steering the same session , but we've found that people just observe how others use it and follow those social norms — it's been pretty intuitive for us to integrate Claude Tag into our teams. Thariq: It's great for teaching people, and also for reducing slop, because the fact that everyone is seeing you use Claude together sort of levels up how you use Claude as well . This reminded me of how Midjourney solved the challenge of teaching people advanced image prompting by enforcing prompting in public in their Discord channels. Something I've found really hard myself is knowing when a feature is worth shipping now that the cost of actually building features has dropped so much. Simon: How do you deal with the hardest problem in all of engineering — prioritization? How do you decide which features are worth building and shipping when building a feature is so much more inexpensive now? Cat: This is the hard thing. There are a few ways we approach it. One is we dogfood our products every single day. Whenever there's something we want to be able to do in our products that we're not able to, instead of finding a different solution we fix our product so it can support that case. We have a very heavy dogfooding culture internally. Before we share our products with everyone in the world, we share them with everyone within Anthropic, and with some early customers who give us very honest feedback about it — the more brutal the better — and we iterate until people love it. We have an internal bar for the number of active users and the amount of retention a feature has to have before we share it with the world. Because this bar is very clear, every engineer knows what they're trying to hit. I think this also levels up our polish, because if the feature isn't polished, people will churn — and then we shouldn't ship that feature. Using internal user-retention to decide if a feature should ship makes a whole lot of sense to me. Simon: Do you have an example of a feature which surprised you? You rolled it out and the engagement was off the charts — something unlikely to be shipped that turned into a real product thing. Cat: I do have one. A lot of folks on our team love remote control . Remote control lets you use your mobile device, or Claude in the web browser, to connect to a local Claude Code session running in your CLI. I never have this need, because I just kick off the task directly on mobile and it runs in a cloud session without using my local environment — I think because I'm doing very easy coding tasks. It was something I didn't totally understand; I was like, hey, people should just set up remote dev environments. But in practice, once we rolled out remote control, so many people I talk to told me that what they do every night is plug their laptop into a power charger, open a bunch of remote control sessions, lock the screen, and then use their mobile phone from their couch to control Claude Code . So this has become a flow we're now leaning into that I didn't originally get — but now I do. One of the over-arching themes of the conference was review: how much attention to people spend to reviewing code written for them by coding agents. I was very keen to hear the Claude Code team's take on this! Simon: How does code review work? Does a human being review every line of production code that makes it into Claude Code? And if not, what are you doing — how do you keep the quality up? Thariq: It varies on the task a lot. For important areas we have code owners. The system prompt is an example where we have a code owner — you really need to get their approval. Simon: So the code owner is directly responsible for the quality of that area of the code. Thariq: That's right. Cat: And they need to approve any PR that touches it. Thariq: We have our code review GitHub bot review everything — that goes on every PR, and often it's doing the bulk of the review. Something I've seen on the team is that for more complex PRs you might make an artifact to explain the PR so that other people can then review. And we invest a lot into verification, CI/CD, things like that, to make sure that any time anything fails we have a test. We have a really robust environment where Claude can control Claude Code and test it. So there's a multi-pronged approach to code review. Cat: In general, we are trying to move to a world where humans don't need to be in the loop . For the most critical changes to the core of Claude Code, and the cores of other products, there is always a code owner and they do manually review all the changes. But increasingly, for the changes at the outer layers, we actually have Claude code review fully review those . That sounds pretty scary, but we've had a six-plus-month-long process to get here, and there are baby steps that you take to build up trust with code review . In the beginning we had human review for everything, and then increasingly we would say, okay, for code changes that touch these files, code review is catching 100% of the issues there — so we actually don't need a human manually reviewing those . And when we have incident review, we look at the PRs that caused the incident and say, okay, how do we update code review to catch that? — and we take those PRs and add them to an eval set to make sure our future changes to code review never regress that metric. Removing humans from the code review loop is a big step forward. It can sound scary, and it's not something you can do overnight, but it is something you can do through many months of investment in the infrastructure to give you the confidence that code review is catching everything you care about. So the key seems to be constantly iterating on the automated review systems themselves, in order to build trust in them over time. We got deep into evals - another hot topic throughout the wider conference. Simon: I know that Opus 4.8, if I ask it to build me a JSON endpoint that runs a SQL query and outputs JSON, is just going to get it right — that's not something I have to review closely. But then a new model comes along and I don't know how to build trust in Fable quickly, that it's not going to mess things up that Opus didn't. How does the new model affect your intuition for what it can do and what it can't do? Cat: The main reason we're building up this eval base over time is so that new models can be a drop-in replacement . When we have a new model, we run the whole eval set and make sure that, for example, Fable is strictly better than Opus 4.8 — and that gives us the confidence to drop it in. Simon: Are those model evals for Anthropic as a whole, or Claude Code team-specific? Cat: We have both. We have evals on our team, and we run code review across every repo within Anthropic, so we have evals for that. And for things like auto mode, we not only have evals across every user within Anthropic — we've also commissioned multiple external testers to red team it, to create environments with prompt injections and malicious inputs, and make sure that auto mode doesn't let any of those pass . Simon: I want to know if the system prompt improvement I made actually improved the product — that's the most basic form of product-specific eval, and I still don't have a great feel for how to do that. Is that something you're doing such that you have complete confidence that a tweak you've made to the system prompt results in better output? Cat: We don't have complete confidence, but we do a lot to make sure that we don't regress performance. The starting point is a suite of external evals that we trust, and we complement that with an even larger suite of internal evals that we trust. To start, we mainly optimize for capability : given a complete definition of a task and the full codebase, does Claude make the right decisions, fully fix the bugs, and pass all the tests? That's the starting point and the thing we optimize for, because it's most directly what users want. But there are a lot of behaviors that impact how users feel when they work with Claude Code. For example, people really don't like it when Claude Code says it's time to go to sleep. Or people really don't like it when it says, "Hey, I finished two out of five parts — do you want me to continue?" Yes, please continue. So we're building up a set of behavioral evals to catch these. And as we get user feedback — please be loud with us about your user feedback — we rank the priority issues and go down one by one and build evals for each of them. It's not 100% coverage, but it is a priority for us to increase the coverage. Simon: How much interaction is there between the Claude Code team and the teams at Anthropic who are training the models in the first place? Is that quite a close collaboration? Cat: Across Anthropic, we all work quite closely together. We meet often to talk about what we expect the next generation of models to be able to do. Our research team has also been amazing about showing this publicly — we often talk in our blog posts about how we're targeting ever-increasing longer-horizon work , and how we train Claude itself to be honest, harmless, and helpful. We also put a lot of effort into making sure it's aligned with your intent, even if your intent is expressed in a fuzzy way. Of course, try your best to be specific about what you want, so Claude has all the context — but even when you're not specific, we teach Claude to make good assumptions. It's been a productive partnership. So many useful prompting tips in this section! Simon: Thariq, you mentioned this morning that the system prompt for Claude Code has been reduced by 80% because of Claude Fable . Can you go into a little more detail? What kind of things have you been able to drop? Thariq: It wasn't just Fable — it was Opus 4.8 as well, and going forward, future models. We have different system prompts for different models now. One of the patterns we saw is that we were over-constraining Claude. The initial, maybe Opus 4-ish models wanted a lot of examples, and removing examples was extremely helpful , because it was just more creative than the examples we gave it. Simon: That's really interesting, because one of the top prompting tips I give people is: give it examples. If that's no longer true, that kind of breaks my prompting model a little bit. Thariq: Same here — I was surprised to hear that. I think now it's more about the shape of what you give it — the tools you give to Claude, your system prompt, things like that. The other thing we did is try to give it more context and fewer "do not do this" instructions, because that's a very strong impulse for Claude, and especially if it conflicts with user instructions later on, that can be extremely confusing to Claude — "I've got this skill that says this and the system prompt says this." So we try to have fewer hard constraints, more context, and fewer instructions overall . It's definitely a science — it took a bunch of evals to build. Cat: In general, when you're prompting these models, you should always think: are there edge cases to the instruction that I'm giving it? When we went back and reviewed all the instructions in the Claude Code system prompt, we found a few cases where yes, this statement is 90% true, but there's a real 10% of cases where it's not true . We didn't want to constrain the model, or confuse it into thinking it should always do this. One good example is verification. Everyone here wants Claude to verify its work, and we had some instructions in the prompt that said: if you make a front-end change, always verify. But there's a limit to it. If it's changing copy from one string to another string, and the user says "just make a quick fix and update the test," maybe you don't want to verify. So we've adjusted our wording from "always verify, verify, verify" to something like: most of the time when you're doing front-end work you can't fully understand the experience by hitting the backend endpoints, so when you make larger changes to the user experience, please run the app locally. And in fact, that instruction probably isn't even good either, because what is a large change? Maybe it should test small changes too. In general, whenever you give a prompt to the model, you should think about the ways in which it could be misinterpreted by a well-intentioned human , in order to better understand how the model might interpret it — and soften the prompt so that it's actually 100% accurate, because you're giving this prompt to the model 100% of the time. Simon: What's fascinating about that is you're relying on the model's judgment — and that's got to be an Opus/Fable-level thing. Models a year ago did not have the level of judgment necessary to decide whether they were going to test a change or not. But that does break down if you're building for a wide range of models and trying to run the cheaper models for cheaper tasks. Cat: We actually have a different system prompt per model now , for this very reason. It's only our most frontier models that have this 80% token decrease — the older models still have the full system prompt. Simon: Do you think Fable and Opus are smart enough to prompt Haiku with more details, because they understand that Haiku has less judgment, less taste? Cat: We haven't been able to eval it — we don't have any hard data to show it. Thariq: There's a tough thing with smaller models sometimes, because sometimes the larger models can be more token-efficient on a hard problem than the smaller models . So there's a bit of intuition to build there — sometimes you really just want frontier intelligence almost all the time. The Pareto curve shifts, and it's hard to find. Simon: A year ago I did not trust a model to write a prompt. Today the good models are very good at prompting — a lot of my prompts are written by models, which feels absurd but works really well. What helped me come to terms with that was thinking about subagents, which are entirely about a Claude model setting up a prompt for another Claude model. Thariq: Workflows are actually a really good example of this, because it's Claude not just prompting a single subagent, but prompting the orchestration of many subagents, and each one of them gets a very detailed prompt. It's almost a level above just spawning a subagent. I've also been using it on my personal machine, giving it the Gemini API and saying: here, generate images . It's way less lazy than I am at prompting an image model. It's just Claude prompting Claude all the way down. Cat: I think Claude also wrote the prompt for the workflow tool . Simon: I've read that prompt — it's a good prompt. That's actually a frustration I have with Anthropic generally: you publish the prompts for Claude Chat , but you don't include the tool prompts and the Claude Code prompts. I still have to run a proxy to intercept them. I would love it if the Claude Code prompts were deliberately published — they're the documentation. They're how you know what the tool can do and how it works. Cat: I'll write down that feature request. I'll have Claude Tag do it. Interesting to note that OpenAI's prompting best practices for GPT-5.6 includes similar advice for their latest models: Favor leaner prompts Removing repeated instructions and examples and simplifying tool descriptions can improve task performance and token efficiency. In a sample of internal coding-agent eval runs, configurations with leaner system prompts improved evaluation scores by roughly 10–15% while reducing total tokens by 41–66% and cost by 33–67%. Simon: Claude Code is basically a big bag of tools. What's your bar for introducing a new tool? How do you decide when it's worth doing that additional engineering at that level? Cat: Do you want to take it? You introduced one of the best tools we have. Thariq: My career peaked when I introduced the ask user question tool. It's really hard. Especially for some tools — ask user question is Claude's tool to ask you — so it's hard to eval, and sometimes it's more of a user preference thing. Back then we had fewer evals, so it was very dogfooding based — or "ant fooding," our ant version of that. But overall we've been trying to trend towards fewer tools . The last set of tools we introduced was the task tool, I think — and we try to give Claude more general versions to do things. I have a long-running fascination with file editing tools - they were the subject of the old Aider code editing leaderboard , and I've watched with interest as they've evolved in different coding agents from search-and-replace based to line-number-based to more complicated patterns. The Claude API docs describe a text editing tool that's recommended for building against the API, but Claude Code seems to use slightly different approaches here. Simon: One of the most interesting tools is the file editing tool — you can have file editing as a tool, or you can tell it to use sed and grep and do things that way. What's the latest evolution of your file editing tool? Thariq: We still have one, but for example we removed our grep and other search tools — glob tools — in favor of native bash. Like I said in my talk earlier, the models are kind of more of a biology than a physics , and tool design especially is quite hard. I'm not sure if Cat disagrees and thinks there's a science to the eval of it, but I think tool design is more of an art, maybe — or a biology. Cat: I largely agree, but in general as we introduce more tools, we try to keep the cardinality pretty low and make sure that every tool we add has a distinct function from every other tool, so that Claude can very easily distinguish when to call each . For file edit, the reason we have it is actually because we can render it. We show people when Claude makes a file change, and there's this nice dedicated UI that says: do you approve this edit to this file? The reason we had a dedicated file edit tool was so that we could deterministically know that Claude was making a file change, so we could show people this nice UI. A lot of new users onboarding still really like this experience, so we've kept it around. But for a lot of us who are on auto mode right now — hopefully you're not on YOLO mode — I don't think it actually matters, and we could probably just remove file edit and be totally fine. It's the prompt injection question! Who better than Anthropic employees to explain how Anthropic sees the risk of prompt injection attacks causing their Claude Code instances to run amok? It turns out they really trust their auto mode - and see that as the feature that enabled Claude Tag. Simon: Let's talk about safety and security. I am deeply aware of the risks of prompt injection, and there are so many bad things that can happen if somebody else tells my Claude Code what to do. I still mostly run Claude Code in YOLO mode and feel incredibly guilty about it. What's the advice within Anthropic for safely running Claude Code? Cat: Why not auto mode? Simon: I am starting to use auto mode, but I don't understand it enough to get how safe it is. As of maybe three weeks ago, I'm defaulting to auto mode. Cat: Broadly within Anthropic, almost every single person uses auto mode. It is the best way to do long-running work in Claude Code while being safe. We've done extensive bashing. We have thousands of evals. We've commissioned many red teamers to create adversarial environments in order to trick Claude Code into doing bad actions, and we've mitigated every single issue that they found. We're going to publish some evals in the coming weeks, but we've pretty much mitigated every attack. Simon: That is a big claim. Cat: We'll share the evals for it so folks can assess, but we've been extremely diligent about identifying all the ways in which Claude might mess up and then updating auto mode to counter it. It doesn't catch 100% of things — that would be way too strong a claim. But for the main categories of risks that we're concerned about, like prompt injection and data exfiltration, the risks are far lower than the average human reviewer . I am very much looking forward to learning more about their evals and approach to verifying auto mode. Thariq: A little on how auto mode works — it's useful to build this mental model. Whenever Claude is doing a turn, or a bash call, there's a Sonnet classifier that is judging the tool call and also the context of the conversation — your instruction. There are some things around permissions that are dependent on your request: you don't want to give git push permissions all the time, but if you say "push this to GitHub," you want it to do it — and if you say "don't push," you want it to deny it. Auto mode will do that. That particular thing happens to me a lot, where Claude tried to do something because it's very helpful and proactive, and auto mode saw "don't do this" and surfaced it. So it's good at the dynamic permissions that you yourself give inside the prompt, which I think is really important. It also works well with our sandboxing infrastructure , because sandboxing is one of those things where there are so many different edge cases that it's hard for us to deterministically follow them. We have a sandbox, and when something needs to escape the sandbox — like a network request — auto mode can look at that request and ask: does this make sense? — and allow it. Simon: I hadn't realized auto mode is interacting with the networking sandbox as well. Cat: It interacts with any permission prompt the user would otherwise see. Simon: How old is auto mode? As a feature I had access to, it's only a couple of months old, right? (It was first made available to the public on March 24th .) Cat: We've been using it within Anthropic since January , so we've been hardening it for quite a while. Anthropic is extremely focused on safety and security, and we've been working broadly across our alignment and safeguards teams to enable the rollout internally, build out these evals, and make auto mode even more robust before sharing it with the world. Thariq: This is also the reason Claude Tag is so good — Claude Tag uses auto mode . I've heard a lot of build-versus-buy questions about a Slackbot, and I'm like: please, you probably shouldn't build your own AI Slackbot. There are so many attack vectors. You have a feedback channel that users can post feedback into, and now your bot is reading it. The work we've put in with auto mode — and we have a general Swiss cheese defense for security; we also RL against this stuff — I think this is really what makes Claude Tag work . It works seamlessly with your permissions, and you don't want to be prompt injected in your Slack. Simon: Are there any more security things in the pipeline that go beyond auto mode? Thariq: I think we're very secure. With Claude Tag you can provision your own credentials for Claude , so it doesn't need to act on your behalf — you can have Claude as an identity, and that also makes it easier to audit and inspect what Claude is doing. Simon: Because Claude Tag is influenced by anyone who can talk to it — it's got a much wider pool of people telling it what to do. Thariq: That's right. And of course we have probes as well with Fable, which is a downstream effect of our safety and research work. I think this is the moment where you see Anthropic being an AI safety company really paying off: we really want Claude to be able to run in an aligned way over long periods of time , and auto mode has to be basically flawless for this to work — it's all downstream of our being an AI safety company. Cat: We also launched trusted devices for the remote control users out there who want to be safer. And for all of our remote environments, we support credential injection . If you want Claude Code to be able to access Datadog, but you don't want Claude Code itself to hold the Datadog credential, you can set up our identity and credential management system so that the Datadog credentials are only usable by the agent but not accessible by the agent — we insert them on the fly when the agent tries to make a Datadog request. I really like that credential injection pattern, where Claude Code can access an API via a proxy and that proxy both audits the request and injects the relevant API key - so Claude can access authenticated endpoints without having access to the API credentials itself. Thariq talked about a sense of grief brought on by Fable-class models in his keynote in the morning, and we dived further into that as part of our conversation. I've been calling this Deep Blue . Simon: Let's talk a little bit about the human element. A lot of people are feeling a sense of loss now that so much of what they considered to be their role in building software is being subsumed by the models. How do you think about that? How has the past year and a half changed the way you think about your own craft and the value that you add? Thariq: Cat and Boris are such good reminders that you have to be more ambitious. They're always like: we're growing so fast, we have to be on the edge, we have to do the best work we can. That's a constant reminder for me — any time I'm slow on something, I'm like, okay, can I do it faster? Can I be more ambitious here? And oftentimes the answer is Claude, because Claude is getting better as you go — the last time I tried this, it was with the previous model. On your point about loss: I think this is real. If you're only trying to do the same work you were doing before LLMs, and now it's a prompt, it is, I think, kind of a sad feeling. And the way you offset that is by being more ambitious. I think Jared is such a good example — he hand-wrote all of the Zig code in his Oakland apartment in about a year, barely left his house, and had so much fun doing that. Now I see him rewrite all of Bun into Rust and he's having so much fun doing that — it's so much more ambitious, and that's how he offsets it. Generally it's asking how do I do the bigger thing and do more — I think success is fun . It's changing your ambition. "The way you offset that is by being more ambitious" neatly captures where I've landed on this issue myself as well. Simon: And Cat, what does that look like from a product management perspective? Cat: I feel like the product role just changes every single month. All the PMs on our team are this mix of engineer, designer, PM — most of them actually used to be full-time engineers. For us it really means plugging in whenever there's any kind of gap . If we have an idea and we didn't inspire any engineer to go build it, then we should just build it, put it into a notebook, and inspire people to take it to production. If the designs look a little off, let's take a page that's similar, do a first-pass design, and tag in someone who's very detail-oriented to fill in the gaps . Or if we notice that our team and product adoption is bigger within the company, and more people need to know what's coming down the pipe for Claude Code, Claude Tag, and Cowork — let's automate figuring out our whole launch calendar, let's automate getting those status updates asynchronously so we're not bugging people, and make sure our updates in our internal announce channels are fully detailed and to the point. For us it's very much understanding what the gap is right now between a great idea and getting something to our customers , and how do we automate it as much as possible . This reflects something I've noticed: when you can produce code so much faster, time spent blocked awaiting a decision from someone else becomes a much more notable bottleneck. Engineers who can make product decisions can move a whole lot faster, and the cost of getting one of those decisions wrong is much less prohibitive. Simon: What's a moment when Claude has surprised you? When the model did something you didn't think it would be able to do? Thariq: I've posted a lot about Claude video editing, but most recently I gave a talk at the ACM Agentic conference, and I asked, "Hey guys, do you have the edited video? I'd love to post it and share it with my comms team." They said, "Oh, it's taking so long." So I asked for the raw files. They sent me the video of me talking on stage, the video of the deck, and the audio file, and said, "Good luck." I gave this to Claude, along with my HTML deck, and said, " Hey, can you just edit this together? " And what it does is honestly incredible — I'm ready to ship it. It transcribes the entire video. It notices that sometimes the video of my deck is a little weird — there's a popup of an auto-update in the middle — and it goes, " Oh, I probably shouldn't use the video of your deck. What I'm going to do is slice it up, figure out which slide you're on, and use the HTML source instead. " So it displays the HTML source. Then it's got video of me, but I'm only taking up a small part of the stage, so it's cropping dynamically to where I am on the stage — and I'm pacing, so it's tracking me as I pace. And it's transcribing what I'm saying. Simon: This was Fable, right? Thariq: This was Fable, yeah. It was a good prompt, but it was a one-shot prompt. Then I asked it to add some interesting animations and graphics, and I was just blown away. It does ffmpeg, it does Remotion. Here's Thariq's video on how he used Fable to edit Fable's own launch video , and here's that launch video . I'm embarrased to admit that I've been finding it quite hard to come up with tasks that frontier models like Fable 5 and GPT-5.6 are unable to accomplish. Cat still doesn't rate its UX design skills: Simon: What can't it do? What are the things where you're still disappointed — where you're waiting for Claude Fable 6 to figure it out for you? Cat: I want it to have better design and UX taste. It's now at the point where if I write out a prompt with a detailed spec of how I want a feature to behave, it will usually behave that way. But the paddings might be off, or the interface just isn't delightful yet. It leans on existing best practices for how apps are designed, but for frontier AI products, there are so many new interaction experiences that we have yet to design . Simon: There's an Opus aesthetic — you can look at something and go, "Yeah, that was designed by Opus." It'd be good if we could move beyond that. Cat: Yeah. I'm very excited for future models to hopefully be interaction design thought partners . Thariq: What can't it do? I would love to see it interact more with the real world. Can it solve science? Can it orchestrate the experiments? There's some amount of coding that goes into that, but there's also this other taste of the broader world that it needs. I figured this would make a great closing question: Simon: Which parts of Anthropic's company culture do you think uniquely help Anthropic be productive with these tools, that other companies should steal? What are the cultural hacks people should be adopting from you? Cat: I'll share one for Claude Tag. Claude Tag works best when you have it in a public channel, and when most of your channels are public. Claude Tag is able to search across all public channels to get as much context as possible to give you the highest-accuracy answer — and it's only able to do this if it has access to everything . Thariq: I mentioned this in my keynote, but it's so important to me I want to re-emphasize it. The co-founders say we don't negotiate against ourselves , and I think this is really important. You can imagine trade-offs in your head and talk yourself out of doing something ambitious — or you can just try to do the ambitious thing. We're so often asking: what if we just did it? Is this a real trade-off or not? And if so, why — where's the proof that it's a real trade-off, and not just something that sounds reasonable? Make the trade-offs show themselves to you. Be as ambitious as you can. I couldn't resist throwing in this one as well. Simon: What's one of your favorite absurd things that you've built with Claude, just because you could build it? Thariq: I'm working on a 2D Street Fighter fighting game with me as a character — and my friends as well. It uses Claude Code to prompt Gemini — and honestly the Seedance model is pretty good — to make video animations. It works great; it's so good at prompting, and it can verify the frames to check whether an animation was good. Simon: Is this Street Fighter 2-level 2D sprites you're generating? Thariq: Yeah, exactly — 2D sprites. The animation looks amazing. And it can also figure out hitboxes — it can be like, "Oh, your fist is here, I'll draw the JSON hitbox." It's incredible. Cat: Mine is much more simple. I'm a big rock climber and a lot of my friends climb, so we have this little app we built with Claude Code where we log all the projects we're working on. We also go outdoors together a lot, so we have Claude do all this research with workflows. Workflows is amazing — we brand it as a coding tool, but it's amazing for doing deep research for travel. I also plan our team offsites, and it's good at finding venues that can fit all of us. I use workflows to research all the climbing destinations we might want to go to, and what has direct flights from where all of us are located. It goes to Mountain Project and finds all the climbs at our grade level. It finds the Airbnb. And I don't like hiking, so I care a lot about it having a very short approach — very short walking distance from where the car parks to where the rock actually is — and it filters for this. With existing apps I have to manually click through Mountain Project, but with this I just put in all of our preferences and it's a custom app for us. Simon: So you're basically vibe coding Jira for mountain climbing. Cat: Exactly. We had a few minutes at the end for questions from the audience. Audience: Do you have any near-term plans to build more eval tools for us to build eval datasets, and more observability tools to monitor the performance of agents and workflows? Cat: We've considered building eval tools, but I think the limiting factor actually tends to be that it takes a long time for customers to build really high-quality evals . So I think the tooling is less of the constraint, and more the skill set of how you build a great eval. That's an area where we're excited to both invest internally and hopefully share some best practices externally. Audience (Sai): I'm interested in the memory and the multiplayer. How is memory being designed today? I assume it's around files. And second, have you thought about an orthogonal direction where you would actually need a data store for these memories, instead of files, to scale it better? Thariq: Right now for Claude Tag the memory is channel-specific. Every Claude in that channel has a shared memory, and the instances have a session — but the session can contribute back to main memory. We do a lot of memory research, and it can be kind of unintuitive what the right way to do memory is. We're always running memory experiments. How it works right now in Claude Tag is a markdown file per channel. You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options . Claude Tag (Claude's new collaborative Slack integration) now lands 65% of the product engineering PRs for the Claude Code team. Claude Code ships features to Anthropic employees first, and only ships the features that demonstrate user retention with that cohort Critical changes to Claude Code are still reviewed manually, but the team increasingly relies on automated code review for the "outer layers" of the product. Adding examples to a system prompt is no longer best practice for models like Fable 5 or even Opus 4.8. The Claude Code system prompt recently reduced in size by 80% . Likewise, lists of " don't do X and don't do Y " can reduce the quality of results from the latest models. Dogfooding inside Anthropic is called " ant fooding ". Anthropic really believe in their auto mode , and see that as an enabling technology for Claude Tag. Thariq advises offsetting coding-agent-induced Deep Blue by " being more ambitious " with the work you take on. Fable is competent at editing video , and Thariq used it to edit its own launch video. Anthropic's culture of working (internally) in public is key to their success, as demonstrated by the way they use Claude Tag in their public Slack Channels.

0 views
Filippo Valsorda 1 months ago

Opaque, Interoperable Passkey Records (and a Go API)

Passkeys are the most important thing happening in information security right now because they are the only principled solution to the overwhelming effectiveness of phishing attacks. Just like memory safety is the only principled solution to memory corruption attacks. Unfortunately, implementing them on the server side can appear more complex than using password hashes. Part of this is unavoidable because passkeys require interaction with the browser to get their phishing resistance properties. Part of it, however, could be abstracted away a little more effectively by defining interoperable passkey record encodings. The WebAuthn specification defines a credential record as an abstract concept with a number of components such as , , , , , and other flags. Google recommends a database table with a Credential ID primary key, and , , and columns. Adam Langley’s stellar Tour of WebAuthn similarly recommends a primary key, and separate and columns. All guidance recommends using libraries to handle WebAuthn authentication, but that still leaves applications with a potentially non-interoperable database schema. It might also be impractical to defer the whole authentication flow and database interaction to a library or framework. Interoperable, well-specified passkey records that the application can handle as opaque strings, like password hashes, can be a middle-ground abstraction layer. c2sp.org/passkey-record is a specification proposal which borrows the syntax of Password Hashing Competition (PHC) Strings and reuses the existing authenticator data encoding for the bulk of the work. A record looks like this: The payload is the authenticator data, a binary encoding of most of the credential record fields that is already specified by WebAuthn and included in the JSON encoding of an AuthenticatorAttestationResponse (which is the return type of even if attestation is not in use). Transports are the only missing field, and they are stored as PHC parameters. The application is then only in charge of keeping track of what passkey records are associated with a user account, which is a task that is familiar to web developers because it is not dissimilar to implementing password authentication (except there are multiple passkeys per account). These opaque strings can be passed to a library to verify the login assertion (or to generate a registration request with the appropriate ). With a well-specified interoperable storage format, it will hopefully be possible to switch passkey library (or even backend language) while preserving a credentials database. Along with the passkey record, applications might still wish to store metadata fields like a user-selected nickname and creation and last use timestamps, to provide pretty passkey management UIs. None of these require any special treatment by the WebAuthn library. The one exception is the backed up state flag. Passkeys report to the server whether they are backed up (e.g. to iCloud Keychain or a Google account), and servers can use that signal to suggest removing a password from an account. This flag can change across logins, while the passkey record is immutable, so it would have to be stored separately and updated on every login. I think this logic is overrated for the average website, which will keep supporting email password resets anyway. Building on top of these passkey records, I drafted a potential stateless Go package API . The registration flow is The login flow is The application is in charge of The library provides JSON values that can be passed straight to and , and accepts JSON values returned by calling on a PublicKeyCredential . This is optimized for the discoverable credential flow (a.k.a. passkeys, where the authenticator stores and provides to the server the user ID) but the method can also be used for second-factor flows or re-authentication prompts. The API works for both modal and conditional UI (autofill) flows. There are a few helpers to extract information from the passkey record ( , ) and from the JSON-encoded PublicKeyCredential ( ). Currently, there is no implementation; I would like to get feedback on the passkey record format and on the Go API , before potentially making a proposal for Go 1.28. One thing you can’t do with this storage model is ensure that different accounts don’t share passkeys with the same Credential ID, which the spec says you SHOULD do. The reason for that check is avoiding attacks where you look up a credential by its ID and land at the wrong public key or user ID because the attacker intentionally injected a colliding Credential ID through their own account. This attack simply can’t happen if you don’t have a Credential ID index in the first place! The index is only needed to mitigate an attack introduced by the existence of the index. Login attempts carry the user ID, and if you use that to look up the user’s passkey records to verify the login against, it doesn’t matter if some other user has a passkey with the same Credential ID, just like it doesn’t matter if two users share a password. Don’t let the attacker dictate your PRIMARY KEY and you won’t have PRIMARY KEY collision attacks. For more Go API previews, follow me on Bluesky at @filippo.abyssdomain.expert or on Mastodon at @[email protected] . More from this year’s CENTOPASSI (a GPS-tracked motorcycle competition involving careful planning, 100 coordinates, and 1700 km of secondary roads over three and a half days). Here’s a glimpse of Castel del Monte (AQ), after climbing down from a deserted and still snowy Campo Imperatore. My work is made possible by Geomys , an organization of professional Go maintainers, which is funded by Ava Labs , Teleport , Datadog , Tailscale , and Sentry . Through our retainer contracts they ensure the sustainability and reliability of our open source maintenance work and get a direct line to my expertise and that of the other Geomys maintainers. (Learn more in the Geomys announcement .) Here are a few words from some of them! Teleport — For the past five years, attacks and compromises have been shifting from traditional malware and security breaches to identifying and compromising valid user accounts and credentials with social engineering, credential theft, or phishing. Teleport Identity is designed to eliminate weak access patterns through access monitoring, minimize attack surface with access requests, and purge unused permissions via mandatory access reviews. Ava Labs — We at Ava Labs , maintainer of AvalancheGo (the most widely used client for interacting with the Avalanche Network ), believe the sustainable maintenance and development of open source cryptographic protocols is critical to the broad adoption of blockchain technology. We are proud to support this necessary and impactful work through our ongoing sponsorship of Filippo and his team. call with the logged-in (or otherwise identified) user details and any existing passkey records pass the returned JSON to and then to pass the returned JSON-encoded PublicKeyCredential to store the returned passkey record in the database call while generating the log-in page store the returned request in a key-value cache with a short TTL, under , and pass the returned JSON to and then to pass the returned JSON-encoded PublicKeyCredential to , and use the returned requestID to retrieve the request from the key-value cache and use the returned userID to retrieve the passkey records from the database pass the JSON PublicKeyCredential, the request, and the passkey records to associating an opaque, permanent, privacy-preserving user ID with each user; storing passkey records associated with a user; and caching request challenges produced by .

0 views
Unsung 1 months ago

The Swiss Cheese model, pt. 1

Have you head of the Swiss Cheese model ? You see it sometimes in descriptions of how complex systems fail. The visual usually goes like this: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/the-swiss-cheese-model-pt-1/1.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/the-swiss-cheese-model-pt-1/1.1600w.avif" type="image/avif"> The whole idea is: even if you have multiple layers of safety – like many slices of cheese – there are always holes in each slice. Typically, a hole in any slice is covered by a non-hole in the previous one or the next one. (For example, a car might not allow you to grab your keys if you have not shifted to park – or, if you start driving with a handbrake on by accident, the car might yell at you.) But occasionally, the holes just happen to line up, and a larger disaster strikes. The model is used in analyses of past accidents, and prevention of future ones. It has proponents and detractors. It’s hard to talk about its applications because the most common examples are horrific. In my book, I wrote about Therac-25 , and that was a really unpleasant chapter to research and to write. Other go-to case studies are equally bleak: Chernobyl , Challenger , the Tenerife airport disaster , the Deepwater Horizon explosion . But I wanted to share it because in my head it applies to UI design also, and sometimes helps me think of how small details add up to a larger whole. In this first part, let’s start with a more traditional example, although a non-drastic one. Here’s a story of Knight Capital Group, a financial services and trading firm. I’m going to hand off the summary of the accident to Henrico Dolfing : On the morning of August 1, 2012, Knight Capital Group opened its systems for what should have been a routine trading day, yet within minutes the firm began sending a flood of unintended orders into the U.S. equity market, buying high and selling low across dozens of stocks in a pattern that made no economic sense and could not be stopped through normal controls. What initially appeared as unusual market activity quickly escalated into a systemic failure inside one of the largest market makers in the United States, with algorithms behaving in ways that neither traders nor engineers could fully understand in real time. […] By the time the issue was identified and the system shut down roughly 45 minutes later, Knight had generated more than 4 million executions across 154 stocks, covering approximately 397 million shares, and accumulated positions worth billions of dollars, resulting in losses of more than $460 million […]. The scale of the incident was not only financial but structural, as a single deployment failure had propagated through a system responsible for a meaningful share of U.S. equity trading. […] Here’s what happened. Long ago, the firm built a pretty boring function called Power Peg to automate some transactions. The function used a standard shared limiter that made it stop executing when all the required transactions were fulfilled. After some years in use, the function was deprecated in 2003 and stopped being used then, but crucially, the code was never actually removed. Some time in between 2003 and 2012, the limiter functionality was upgraded, and the old code stopped being compatible with it. All code in production was rewritten to use the new limiter, but the Power Peg feature wasn’t, as it was already deprecated and not in use. In 2012, the firm started writing a new program for automated transactions. Its creators decided to reuse the same software flag that previously activated Power Peg. The existence of the old code was known at the time, and the idea was that the code would be overwritten by the new program, so the reused flag would only trigger new code. In July 2012, the new code was finished and the firm started to install it on all the servers, overwriting Power Peg. The firm intended to deploy the new code to all eight servers, but a mistake resulted in it only arriving on seven servers. No one caught that mistake. At this point, you can piece it all together. At this point, I imagine many of you have been wincing more and more with each passing paragraph. On August 1, the flag for the new functionality was turned on. Everything was fine on seven servers, but on the eighth one, the flag reawakened dormant code that immediately started executing. As the old code was not compatible with the new limiter, it was never limited, cascading into millions of transactions in less than an hour, and a lot of collateral damage; the subsequent market reaction to the news of the firm losing over $400 million and caused its own stock to tank. Oh yeah, I didn’t mention this yet – the failure killed the firm. (Well, it resulted in a merger, but that seemed to be a way to save face after Knight Capital Group almost went under.) How does the Swiss Cheese apply to this? You can see it as five holes in five different slices of cheese: What’s important to understand about this model is that either of these in isolation would objectively be a small mistake, and caught by the other slides. As a matter of fact, any four of these happening would still not add up to a catastrophe. But in this case, the five mistakes lined up perfectly. Many analyses of such accidents blame a single event in the chain – in this case often the sysadmin that didn’t deploy the code to eight servers properly – but this is primarily because we like stories of individual agency, and are not well equipped to understand stories of systems . (Even Star Trek added a Borg Queen, after all.) That’s the accident eventually dubbed “a Knightmare.” More examples of systems closer to our hearts in following parts. #bugs #definitions it was a mistake to not actually remove the old code it was a mistake to reuse the same flag it was a mistake to not deploy to all 8 servers it was a mistake to not have a procedure for someone to double check the deployment it was a mistake to not have a way of auto-detecting (and perhaps auto-stopping) the runaway processes when the regular limiter failed

0 views

9 months in: building an advanced StarCraft reporting tool with Go & Claude

The story of how I built screpdb, an advanced StarCraft: Brood War replay reporting tool, using Go & Claude over 9 months, and what AI could and couldn’t do along the way.

0 views
Anton Zhiyanov 1 months ago

On interactive Go tours

Over the past two years, I've published interactive tours for five Go releases, from 1.22 to 1.26. I know some of you have read them, and I've received a lot of kind words from you (even some core Go team members reached out) — thank you so much for that! Tour history: Go 1.22 • 1.23 • 1.24 • 1.25 • 1.26 + Go features by version Unfortunately, at some point, writing these tours stopped being fun and started to feel like a part-time job. I'm not really excited about that, so I've decided to stop. I still like Go (well, most of it). I read a lot of Go code, I write some Go code, and I write Solod code, which is also Go 🙂 (Solod is a systems language with Go syntax and a Go-like stdlib). I'm still pretty close to the language and will probably continue to write about it. But the interactive tours story is over.

0 views
Anton Zhiyanov 1 months ago

Go-flavored concurrency in C

Go's concurrency is one of the main reasons people like the language. You write , send values through channels, and the runtime scheduler runs thousands of goroutines on just a few OS threads. It feels effortless. None of that machinery exists in C. Which made me wonder: how close can you get to Go's concurrency model using only POSIX threads? Obviously, native OS threads can't match the efficiency of lightweight goroutines, but what is the actual cost, when does it become a problem, and is there any way to at least partially avoid it? I ran into these questions while adding concurrency to Solod (So), a strict subset of Go that translates to plain C, with no runtime and no garbage collector. In the end, I came to the conclusion that you can do quite a lot with pthreads — as long as you're honest about the tradeoffs. This post is about the POSIX threads-based concurrency model I chose, the benefits it offers, and its limitations. Mutex/Cond • Atomics • Pool • Channel • Performance • Design • Wrapping up Everything in So's concurrency stack is built on two basic POSIX primitives: the mutex and the condition variable. is a thin wrapper around : Since So translates to C, this is basically a struct that holds a and a function that calls . Here's the transpiler output: That is the whole translation — the generated C is a near-mechanical mirror of the So code, only noisier. From here on, I'll mainly show the So version, but I'll also provide the C code for those who are interested. There's nothing exciting here: is a pthread mutex wrapper that panics if something goes wrong (which is rare). The companion primitive is , a wrapper around . It's the standard "wait until a condition holds" tool, associated with a mutex: These two types — and — are the foundation. Other concurrency tools — , the thread pool, channels — are built using a mutex and one or more condition variables. This has several effects on performance, as we'll see later. Not everything needs a lock. So's mirrors Go's: , , , , , and a generic , all with , , , and methods. The nice thing is that these don't need pthreads at all. They map directly to the C compiler's builtins — the same hardware instructions that Go's compiler emits. So there's no reason for them to be any slower, and they're not: Each number is the cost of one operation on a single thread. is a good example of using atomics effectively. Its fast path only needs a single atomic load — after the given function runs, every future call to checks a flag and returns: To actually run code concurrently, you need threads. The type wraps and its related functions: Consider this function: Usage example: It might look like , but that's just on the surface. starts an actual OS thread, not a goroutine. You have to eventually call to join or it, or else its resources will leak. Also, OS threads are expensive to create — they're nothing like Go's goroutines, which only need a few kilobytes of stack and start up in nanoseconds. That's exactly why you usually don't want to call inside a loop. For tasks that are short-lived or happen often, it's better to use a pool of long-lived worker threads and send tasks to them. to the rescue: Usage example: The first argument to , , is a memory allocator. Solod avoids hidden allocations, so anything that needs memory takes an allocator explicitly — here it backs the pool's task queue. Under the hood, a is a fixed group of worker threads that pull tasks from a shared queue (a ring buffer). It uses one mutex and a few condition variables: wakes up a worker when there are tasks to do, applies back-pressure when the queue is full, and lets know when everything is finished. It's a classic producer-consumer setup, about 200 lines of code , and there's nothing fancy about it. The heart of the pool is the worker loop. Each thread blocks until a task appears, runs it outside the lock so workers execute in parallel, then records that it finished: This is what separates a pool from a plain queue. bumps as it enqueues; each worker decrements it after running a task, and the last one out broadcasts . sleeps until the count hits zero: The tradeoff is that the number of worker threads is fixed. In Go, a program can handle thousands of concurrent I/O waits because blocked goroutines use very little memory. A So pool can't do this — if all N workers are parked on a blocking syscall, the pool is stalled until one returns. You have to set the pool size based on the workload, instead of letting the runtime manage it for you. Channels are an important part of Go's concurrency model, and So's gives you something quite similar. Just like in Go, it passes values by copy and comes in buffered and unbuffered flavors: is a thin generic shell over one of two engines, picked at creation time: Buffered ( ) is a mutex-guarded ring buffer with and condition variables — like the queue. Senders block when it's full, receivers block when it's empty. The full implementation also checks for , but I left it out for brevity. is the mirror method: block while empty, pop the next value, signal to wake a sender. It also handles the closed channel, returning once the buffer is closed and drained. The rest is this lock-wait-signal core. Buffer source code Unbuffered ( ) is a rendezvous: each send blocks until a receiver takes the value, copying bytes directly from the sender's stack to the receiver's destination without using an intermediate buffer. is the other half: it waits for a published, unclaimed value, copies bytes straight from the sender's stack into (no intermediate buffer), marks it as claimed, and broadcasts to wake the sender back, creating wakeup #2. One hand-off, two wakeups. Copying directly from the sender's stack is safe because of that second wakeup. is a pointer to , which lives on the sender's stack. While the receiver is reading it, the sender is parked in , so its stack frame stays alive. The sender only returns (and reclaims that memory) after the receiver sets and wakes it up. There's no need to copy into a shared buffer because the source is guaranteed to outlive the read. Rendezvous source code As you can see, the API is pretty similar to Go. Now let's look at the numbers. Here's the main tradeoff: pthread-based concurrency primitives are fast when no one has to block, but they get slow when someone does. And it's always for the same reason. Go schedules goroutines in userspace. When one goroutine blocks on a channel and another wakes it up, the runtime moves them between its own queues — no kernel involved. POSIX threads, on the other hand, don't provide a userland scheduler. When a thread blocks on a condition variable, it parks in the kernel, and waking it up requires a syscall. Every hand-off between threads that actually parks pays the cost of a syscall on both ends. You can clearly see the difference in the mutex benchmarks. With 8 competing threads, it all comes down to whether the waiting threads have to park or not: Each number is the average time for a single / pair. The uncontended benchmark runs on one thread, while the contended benchmarks have multiple threads fighting over the same mutex. Notice that So actually wins the first two benchmarks, and for good reason. So's is a plain call with nothing extra, while Go's adds more overhead — like starvation-mode tracking and a runtime that stays involved because a goroutine can be preempted in the middle of a critical section. When nobody parks, that overhead is the main cost, and the thinner wrapper is closer to the hardware. With an empty critical section (the spin benchmark), a waiting thread grabs the lock while still spinning and almost never parks — So wins by 2.8x. The uncontended benchmark (a single thread, no contention) shows the same thing: less code between the call and the lock, so 9ns versus 14ns. The picture flips the moment threads have to park. Give the critical section about a microsecond of real work (the work benchmark) and waiters exhaust their spin budget and park. Now every hand-off costs a wakeup syscall, and So drops to half of Go's throughput. The work is identical in both cases — the difference comes from the parking cost. Condition variables demonstrate this clearly because they always park: Each number is the cost of one rendezvous round: a single broadcast that wakes every waiter and hands control back, with N waiters plus one broadcaster. Pthread-based condition variable is consistently 7-10 times slower. There's no trick to close this gap — it's just the cost of waking up a real OS thread instead of a goroutine. Channels have the same issue because they're built using mutexes and condition variables: Each number is the cost of moving one value through the channel (send plus its matching receive). The number in parentheses is the buffer capacity. The uncontended case fills and drains a buffer from a single thread, so nothing ever blocks — it's just a lock plus a copy, which gives So a slight advantage. But the moment a producer and consumer actually start handing off work, So has to wake up a thread for every transfer that gets parked. It's worst for the unbuffered channel, where every value is a rendezvous with two wakeups: 23x slower. A larger buffer helps a lot — with room for 100 items, most sends go through without waking anyone, and the gap narrows to about 2x. The consequence is that the larger your tasks are, the better pthread-based concurrency works. If you use a channel for fine-grained, value-at-a-time streaming between threads, performance will suffer. But if you use a channel to pass whole work items to a pool, where each item takes tens of microseconds to process, the wakeup cost becomes negligible. The pool benchmarks on realistic workloads confirms this: Each number is the wall-clock time for 8 workers to process the whole batch. Here, So is within 1.1x of Go. The per-task dispatch cost is still present, but it's spread out over real work, and the performance penalty is pretty small. Benchmarking All benchmarks were run on an Apple M1 CPU running macOS. The C code was compiled with Clang 16 using these CFLAGS and mimalloc as the system allocator: The results shown are the medians from several benchmark runs. Each benchmark ran many iterations, following the same logic as Go's own benchmarking. The Go benchmarks used Go 1.26 and . Source code for both So's and Go's benchmarks: conc • sync Here's a summary of the strengths and weaknesses of the pthread-based approach: If you're looking for "thousands of cheap goroutines", the pthread-based approach will let you down. But if you're fine with "a few worker threads handling lots of tasks", it holds up well. Three decisions influenced the way I implemented concurrency in Solod. Pthreads, not fibers . I know there are coroutine/fiber libraries for C that avoid the kernel wakeup cost — single-threaded ones like neco , and multi-threaded ones like libfiber . A userspace scheduler is exactly what would help to match Go in the benchmarks above. I decided not to use one. I wanted something dead simple — an approach I could explain in a paragraph, using tools every C programmer already knows. The trade-off is that you lose some performance with fine-grained blocking, but in many real-world situations, pthreads work fine if you use a worker pool. For me, keeping things simple is more important than saving a few microseconds during task hand-offs. For now, at least. Standard library, not language . Go bakes goroutines, channels, and select right into the language. I decided to keep everything in the stdlib for two reasons. ➀ It follows So's "no hidden allocations" rule. In Go, quietly allocates a goroutine stack, and allocates a buffer. In So, all allocations are explicit: you pass an allocator to and , and you always know exactly where the memory comes from — whether it's the system allocator, an arena, or something else. ➁ A library is more flexible. Since a pool is a regular value, you can have as many as you need, each sized for its specific purpose. In a multi-stage pipeline where each stage needs a different capacity, you can start one pool per stage, each with its own and , instead of being given a single global scheduler. The language stays simple, and the flexibility is in code you can easily read. Timeouts, not select . Go's waits on several channel operations at once and proceeds with whichever is ready first. Implementing it would require a lot of work — a thread has to register interest on multiple channels, block once, and then wake up when any of them is ready — so I left it out. Instead, offers and , which cover two common uses of with a single channel: What's missing is the ability to block on multiple channels at once and continue with whichever one is ready first, as well as the option to mix sends and receives in the same selection. How close can you get to Go's concurrency using only pthreads? Close enough to be useful, but not enough to really match Go. You can wrap real OS threads with familiar APIs — mutexes, condition variables, pools, channels — and the code will look and act a lot like Go, at least until a thread needs to block. But there's no scheduler underneath, so when a thread blocks, it's an actual thread waiting in the kernel, not a goroutine that's paused for free. That's the main limitation of this approach. What you get in return is brutal simplicity. Every primitive is a thin wrapper with no runtime hiding behind it, so the performance is exactly what the OS gives you: fast atomics, fast uncontended locks, and pooled throughput within ~10% of Go on coarse-grained work. But as soon as you switch to fine-grained, one-value-at-a-time hand-offs, the cost of kernel wakeups becomes the main factor, and you'll notice the slowdown. If you think the pthread approach might work for you, I invite you to try Solod . It includes the and packages, along with many others ported from Go's standard library. ➕ Coarse-grained pooled workloads are within about 10% of Go's performance. ➕ Uncontended locks and spin-friendly critical sections perform quite well. ➕ Atomic operations are as fast as in Go. ➕ The implementation is 100x simpler. ➖ Anything that needs to park and wake an OS thread is much slower than Go's userspace scheduler. ➖ The pool can't handle thousands of blocked waiters like goroutines can. "Do this, but give up after a while" (Go's idiom). "Do this only if it won't block" (Go's non-blocking branch).

0 views
Xe Iaso 1 months ago

The console wars have been lost

Previously I opined that Valve was about to win the console generation . I couldn't have possibly predicted that both Microsoft and Sony would just self-sabotage so hard that they're both going to lose. Between Microsoft's decimation of the Xbox division , slaughtering off the IdTech team , and continued increases of Xbox hardware prices ; there's nothing to really be excited about with the Xbox. Sure their most recent presentation showed off a bunch of exclusives, but none of them really made me think "wow, I should go get an Xbox to play that". Hell, few of them made me think "wow I should go play that" beyond the Halo remake coming out next month (and really I just want to see how much of a trainwreck that is going to be). Microsoft is also starting to double-down on their in-house games being Xbox exclusives, which really doesn't give me much reason to want to play them because I simply can't buy them without buying an Xbox. Sony also has discontinued porting their games to PC because they're not hitting the (probably impossible) revenue targets that they need to make up for big-ticket failures like Concord . I do have a PS5 that has mostly been relegated to gathering dust when it's not playing YouTube and Twitch duty in the living room, it's likely going to be replaced in favour of my Steam Machine whenever that comes in next year. However nothing that's come out in terms of Playstation exclusives is really compelling, and what is compelling enough just isn't that compelling to want to buy it on Playstation as opposed to just getting it on Steam where I can run it on my tower or on the home theatre PC. Sony also has been raising prices and recently announced that they're killing physical media next generation . It's starting to make me wonder if I should even bother getting the next generation of Playstation. If I can't give people physical games as gifts anymore, why should I bother buying the new console? My husband and I both can't remember why we even got a PS5 in the first place, maybe it so that we could do couch gaming without hearing the fan noise or so that the video streaming experience from the NAS could support HDR. We have a Switch 2 at home, it's mostly there to play Nintendo exclusives like Mario Kart World and the Xenoblade series. If those exclusives were available on Steam, we wouldn't buy them on the Switch 2. Otherwise, everything is via Steam or other PC storefronts anyways. Man, Valve really does win by doing absolutely nothing while the rest of the industry shoots itself in the head. I fear for what happens when Gabe Newell retires and the MBA cancer fully infects Valve.

1 views
Blog System/5 1 months ago

Autoconf’s revenge: ad-hoc shell templates

As powerful as Bazel is, sometimes it’s not featureful enough. When using this build system, it’s common practice to wrap it in a launcher script—and in fact, this is natively supported by Bazelisk , Bazel’s native dispatcher that stands for the binary in the user’s . Bazelisk will first download the version of Bazel requested by the project, and then, if exists, invoke it instead of the downloaded binary. is what’s known as a Bazel wrapper and is the point of today’s article. Well, not quite. The actual point of today’s article is to demonstrate a simple trick I learned from the GNU Autoconf and Automake days to implement full-blown conditionals in an ad-hoc template system. But because such trick is trivial once you see it, I have to present it in the context of a modern real-world scenario. So what I’m going to do is guide you through the creation of your very own Bazel wrapper to customize Bazel’s configuration file in ways that the native Bazel tool doesn’t support. Let’s get started. But wait! Take a moment to subscribe. I’m sure you’ll enjoy future posts, and it’s the only way for me to know that they are worth writing in the first place! Template systems are everywhere. Take any static blog generation system and you’ll find some. Take system management tools like Ansible and you’ll find others. Take a cloud orchestration service like Kubernetes and you’ll find Helm. Heck, even Go’s standard library provides a full blown text template system out of the box. There is clear benefit and appetite for these and, surely enough, it’s tempting to use any pre-existing such system in your own project… but if all you need are a bunch of variable replacements, some of which may be only conditionally applied, you can go a long way by not taking any dependencies. A call to or the substring function of your language of choice is all you need. To put this in context, let’s say you have Bazel’s configuration file, which is not very flexible, and that you need to set some arguments based on dynamic values that depend on the environment. E.g. something like this: If you know a little bit about , however, you may squint at that and say: “That’s silly! Make those flags conditional on a configuration and you’re set!” So you try something like this: And… this does not work. Gotcha! Startup flags cannot be placed behind a configuration so there is no way for you to parameterize the JVM’s max heap value passed in . And having to remember to pass from CI all the time is fragile, because you might forget and not get the desired configuration in place. Solving the above is not difficult if we could parameterize the configuration. We might want to write something like this instead: … and then have and be replaced dynamically depending on some runtime arbitrary logic. We can do that via the Bazel wrapper, and this sort of dynamic configuration is a common thing to do from it. So let’s do this. Let’s start with the template logic: Ugly(?) bash syntax but nothing too complicated: The global hashmap tracks variable names and their replacement values. The function inserts a new key/value pair into . (It’s important that the values given to don’t contain -special characters like , backslashes, or the separator we chose—but we control the generation of those values so we are good.) The function transforms the hashmap into a set of arguments of the form and then calls to process the given input file into the given output file. Then, we can plug everything together into a minimal Bazel wrapper: In this new snippet, the function instantiates the file from the contents of via and then calls the actual Bazel binary provided by Bazelisk in . The complexity here may seem overkill, but it’s necessary : while it’s pointless to invoke Bazel in parallel due to its global lock, users will run Bazel in parallel and you must make sure that the wrapper is reentrant. Otherwise, you’ll definitely run into races. The rest of the script in does the actual work to compute key/value pairs to substitute in your now-templated and then delegates to Bazel via . That’s it. This is a barebones implementation of a text template system using bash—and I had to use bash, not sh, to get the niceties of a hashmap —that serves as a launcher. Go try it. By the way, the and nomenclature are inherited from GNU Autoconf’s AC_SUBST primitive . “Great!” I hear you say in a sarcastic tone. “You have just applied string replacements! But what about conditionals, huh? You CaNnOt Do ThAt So EaSiLy!!11!one!” Ah, but you can , and showing you that trick is the whole point of this short article, remember? The necessary insight is that we can use string replacements to comment out lines in the original file. What if we did this: In here, we are defining different configurations for developer workstations and for CI, like we did earlier, but then we are auto-magically picking the default configuration depending on and . How? Well: will expand to the empty string when running on CI and will expand to , so the corresponding lines will be enabled and disabled. And the opposite replacement values will appear when not running on CI. Ta-da! Conditionals. We can make things nicer with a helper function and meta-programming: Don’t panic about that . Just as with the invocation above where we could have issues with special characters appearing in values, we control the arguments to so the is safe. And note that we can even nest conditionals arbitrarily. There is nothing preventing you from doing: Which corresponds to the conceptual equivalent of: Let’s do loops? Sorry no, can’t do! Well akshually… we could do loops. Not by using simple tricks like above, but we could definitely sketch something like this: However, this is starting to look a lot like a high-level parser, not scripting where you glue simpler components together. And if you are headed that way, you are better off transitioning to a proper programming language and a well-known template system. What do you think? Do you hate this already? You can, but note that the whole world runs on this stuff. All of that foundational code behind Linux systems ends up using GNU Automake and GNU Autoconf, and those packages are full of stuff like this in their and files. And you can get very far with just the above constructs if you treat the shell like a real language . The Bazel wrapper that I maintain at work these days grew to almost 1000 lines of code before I pruned a lot of features that had become unnecessary, but it’s still pretty large. We are now transitioning it to a Go-based wrapper for better readability and maintainability… but as we do this, I’m reminded that well-groomed shell scripts give you some flexibility that no other language can match in just a few lines. So, keep things simple. You can do a lot with just a few primitives. As powerful as Bazel is, sometimes it’s not featureful enough. When using this build system, it’s common practice to wrap it in a launcher script—and in fact, this is natively supported by Bazelisk , Bazel’s native dispatcher that stands for the binary in the user’s . Bazelisk will first download the version of Bazel requested by the project, and then, if exists, invoke it instead of the downloaded binary. is what’s known as a Bazel wrapper and is the point of today’s article. Well, not quite. The actual point of today’s article is to demonstrate a simple trick I learned from the GNU Autoconf and Automake days to implement full-blown conditionals in an ad-hoc template system. But because such trick is trivial once you see it, I have to present it in the context of a modern real-world scenario. So what I’m going to do is guide you through the creation of your very own Bazel wrapper to customize Bazel’s configuration file in ways that the native Bazel tool doesn’t support. Let’s get started. But wait! Take a moment to subscribe. I’m sure you’ll enjoy future posts, and it’s the only way for me to know that they are worth writing in the first place! The context Template systems are everywhere. Take any static blog generation system and you’ll find some. Take system management tools like Ansible and you’ll find others. Take a cloud orchestration service like Kubernetes and you’ll find Helm. Heck, even Go’s standard library provides a full blown text template system out of the box. There is clear benefit and appetite for these and, surely enough, it’s tempting to use any pre-existing such system in your own project… but if all you need are a bunch of variable replacements, some of which may be only conditionally applied, you can go a long way by not taking any dependencies. A call to or the substring function of your language of choice is all you need. To put this in context, let’s say you have Bazel’s configuration file, which is not very flexible, and that you need to set some arguments based on dynamic values that depend on the environment. E.g. something like this: If you know a little bit about , however, you may squint at that and say: “That’s silly! Make those flags conditional on a configuration and you’re set!” So you try something like this: And… this does not work. Gotcha! Startup flags cannot be placed behind a configuration so there is no way for you to parameterize the JVM’s max heap value passed in . And having to remember to pass from CI all the time is fragile, because you might forget and not get the desired configuration in place. Basic string replacements Solving the above is not difficult if we could parameterize the configuration. We might want to write something like this instead: … and then have and be replaced dynamically depending on some runtime arbitrary logic. We can do that via the Bazel wrapper, and this sort of dynamic configuration is a common thing to do from it. So let’s do this. Let’s start with the template logic: Ugly(?) bash syntax but nothing too complicated: The global hashmap tracks variable names and their replacement values. The function inserts a new key/value pair into . (It’s important that the values given to don’t contain -special characters like , backslashes, or the separator we chose—but we control the generation of those values so we are good.) The function transforms the hashmap into a set of arguments of the form and then calls to process the given input file into the given output file.

5 views

Now Go Build CTO Fellowship: Season 2

Today, we're releasing the second season of the Now Go Build documentary series. Five episodes featuring technology leaders from around the world solving the hardest problems in healthcare and education.

0 views
Brain Baking 2 months ago

Postcard Teas: A Few Impressions

For almost ten years now, we’ve sworn by Mariage Frères when it comes to shopping for high quality loose tea leaves. The nearest shop, however, is in Lille, which is almost three hours away. Their webshop is crude and doesn’t allow for a taste session before buying hence we did buy our fair amount of misses. Yet we remained faithful: the few times that we diverged from the brand ended up in a disappointment. And then I saw someone claiming that London-based Postcard Teas is “even better than Mariage Frères”. My initial reaction to that was “impossible”. I secretly made a note in my journal regardless. When our stock started to dwindle, I dug up that note and said to myself: what the heck, let’s do something crazy and order elsewhere. Postcard Teas is a small shop in London that sells specialty teas by importing directly from the growers. Their unique selling point is hinted in the name: these growers only have a few acres in which they aim to grow the best quality possible. The result is only a few kilograms of yield each year, yet the average price remains acceptable. Each bag of tea you order comes wich a lovely postcard and piece of art depicting a work from the country of origin. I have no idea where Mariage Frères’s tea comes from and love the fact that with Postcard Teas, this knowledge is accessible—even evident. Besides the location and yield, the back of the postcard even contains the grower’s name and a tidbit of bio. Watch China Minutes’ visit to the small shop to breathe in the atmosphere. Meanwhile, I’ll go prepare myself a cup of their summer Darjeeling. Still interested? Great! Yet people outside UK should be warned as shipping comes with a hefty taxation at the border I didn’t mentally prepare for… Just take that into account when you’re browsing their webshop—and don’t forget to compare prices with your usual supplier per , not the deceptive . With that being said, here are some impressions of the teas we tried out: Golden Darjeeling A lovely dark red tea that goes down very well without being too strong. I usually buy first flush/spring Darjeeling and kind of wish I did here as well as that’s usually milder, but this summer Darjeeling is excellent, even if you accidentally let it steep for too long. Contrary to its spring variant, it also handles heat very well, so I usually set it with boiling water. A pure Darjeeling is usually my go-to in the morning or even right after lunch. This black tea is less black than the cheap green powdered teas bought in supermarkets. 4 out of 5 Blounts—Great. Gianfranco’s Earl Grey The first thing that came to mind after opening the bag is: I hope the strong scent does not reflect in the taste. And luckily, it doesn’t. Mariage Frères’ Roi Des Earl Grey is more purgent, up to the point that they might have overdone it. Gianfranco’s bergamots in Calabria pair very well with Kerala’s small Darjeeling tea farms. The structure and colour of the tea is very similar to the previous one, the Golden Darjeerling. This is because Postcard Teas blends both flavours in their shop in London giving them the advantage of carefully choosing both ingredients. Since I love a good Darjeerling, it’s impossible to resist. I do still prefer Mariage Frères’ more daring lavender Early Grey. 4 out of 5 Blounts—Great. New Assam Chai This is the first tea from Postcard Teas that I like less the more I drink it. The culprit? The particular blend of spices: way too much green cardamon. Cardamon is a spice with a minty freshness that easily overpowers everything else, as it indeed does here. Also, the Assam is cut in finer pieces than I’d wish making this brew very dark and strong. I recognise the need for a strong tea to counterbalance the just as strong spices here but for me it was just a bit too much. Adding lemon and honey helps but only up to a point. I know you’re supposed to drop a few splashes of milk in it but I’m not British nor Indian so I don’t. 2 out of 5 Blounts—Mediocre. This is a traditional curled green tea from Japan called a “kamairicha” tea: instead of steaming the tea to stop the oxidation, kamairicha is roasted in a dry pan. Contrary to most Japanese teas such as Sencha, the typical bitter taste is gone because of this process. Mr Ogasa’s farm in Gokase is only 14 acres big. I’m not a huge Japanese tea expert but I do like this one. I do find it difficult to properly prepare: at more than the tea oxidises and still comes off as a bit too bitter. It’s more evenly balanced than the Senchas I have tried before, but that does mean it can come across as bland. I enjoy this tea the most when I am not doing anything else besides drinking tea. 3 out of 5 Blounts—Good. Miyazaki Oolong This complimentary little bag of Oolong tea leaves from Mr. Takuya Yokoyama tastes like a sweet Sencha instead of a typical Oolong tea. It’s one of the greenest ones with virtually no astringency, as described by Postcard Teas themselves. This is exceptional tea of which only was madein 2025. This is interesting because Oolong is usually made in China, not Japan. The problem is that this tea is very delicate: if you’re working or watching or playing something, you might gulp this down without blinking and afterwards think “what did I just drink?” I think these delicate teas are an acquired taste and require a mindful, peaceful moment of tea but nothing else. But why should I buy this Oolong when I already have the Guri Green? I usually prefer my Oolongs to be a bit more oxidised. I hope I’m not getting slammed for this. Oolong teas have a huge variety in roasting/oxidation/etc and this one ranges in the “barely Oolong at all” category. What I also learned is that for Oolong teas the first steep is usually a “wash” to get to the more flavourful second steeps. Perhaps I should try that for Miyazaki’s tea. 3 out of 5 Blounts—Good. Jasmine Green As mentioned on the postcard: “a delightful Vietnamese tea made with spring-picked green tea from Mr. Than’s tea co-op in the mountain village of Ban Lien in Lao Cai province”. Delightful is indeed the correct word here: this must be one of the best Jasmine teas I have ever tasted. It’s very delicate, never bitter, and after you’ve had a cup, you want to make another. What else can I say? It accepts but you better wait a few more minutes until it cooled down to at least and not let it steep for too long. Of course, our pantry now doesn’t stock the three Mariage Frères jasmine teas we tried, so I can’t directly compare them. They’re all great and completely different from the supermarket-bought Jasmine crap. 5 out of 5 Blounts—Amazing. Related topics: / tea / By Wouter Groeneveld on 29 June 2026.  Reply via email .

1 views