This Week at Zed Industries: #15

August 11th, 2023

Welcome to the fifteenth This Week at Zed Industries! How time flies. As we get into the tail end of Summer, we're closing in on the internal Channels Alpha, further tuning our performance, and adding in some new text manipulation commands. Let's get into it!

Piotr

This week I focused on making further stylistic adjustments to the project and buffer search. On that note, I have also collaborated with Julia to investigate various performance quirks of project search. One notable quirk we uncovered was the occasional occurrence of a brief lock-up during searches, accompanied by a "Searching..." label that persisted for a few seconds. As we've found out, Zed displays search results only once it has fully finished it's search. Yikes! During this past week we have rewritten the local part of our project search routine to report search results as soon as they are obtained. Next week I plan to continue poking at our search implementation with Julia and speeding up other kinds of searches given that new-gained insight. It's great to optimise code and socialize at the same time!

Julia

I spent most of my time this week continuing our experiments with WASM extensibility for the purposes of enabling custom language servers. We still don't have a clear picture of how we want to move forward but we're learning a bunch and having a lot of internal discussions about the topic.

The rest of my time was spent pairing with Piotr on making our project search faster which has been very fun and gratifying. I always enjoy working on improving perfomance and it's been great to get to know my coworker better in the process.

Joseph

I've enjoyed being able to code a bit more the last few weeks. In Zed, I've been focusing on adding line and text manipulation commands, as I've been missing these since switching to Zed full time. Over the weekend, I rounded out the text manipulation commands by adding in case conversions (convert to kebab case, convert to upper camel case, etc.). I feel happy with where we are at in our text manipulation commands now - here's a quick demo:

A demo of various text manipulation commands

I spent a bit of time reworking our top-ranking issues script, as the way it was originally written was very inefficient and the number of requests used grew linearly with the number of issues in the tracker; the script was more frequenlty failing to, as we hit GitHub's rate limit. Now, the script takes only a few seconds to run and should consume a constant number of requests.

Lastly, I started working on an experimental tool, a TUI application, to pull all feedback, from various channels, into one location. We worked on a similar tool a few months ago, but it was abandoned due to various issues associated with its web-based nature. It's always fun getting the opportunity to work in Python.

Kirill

Got back from the vacation in the middle of the week, so not much to share this time.

First, I've decided to pause the file drop endeavour for now: cmd-click in terminal gives me 80% of the feature and some tasks grew more important while I was away.

Speaking about one of those, inlay hints' performance turned out to be not as scalable as desired — Zed with its overall smoothness sets the bar quite high. I am working on one of the first ideas, tracking editor ranges we query for hints more rigorously. This makes me feel a bit better when thinking about the unfinished hints' implementaion blogpost: some things definitely will change for good.

I've also added a small panel for toggling the hints and buffer search, should help with both feature discoverability and keybindings for such toggles. Given how much work happens on hints again, I start to wonder about "simple" ways to implement dynamic hints' features along the way while I have the traction...

Nate

Short update today - Just getting back from a week off last week. Have spent most of the week working with Mikayla on channels, deep in on implementation details and understanding how to make generic components work better with the nuances of Rust. Otherwise, I've been supporting Nathan's theme explorations, and other misc things.

Mikayla

This week, let's talk about the Channels security model and a massive privilege escalation vulnerability I discovered while writing last week's post. There's an additional rule in this system that I had considered so self-evident that I forgot to mention it: when creating a new root channel, you automatically get the Administrator role on that channel. To illustrate the problem, take the personal re-organization example I gave, annotated with my membership role:

- #my-freelance-design-business - (Admin)
  - > #crdb-industries/#design  - (Member)
  - > #zed/#design              - (Member)

With the cascading roles system, my Administrator role in my own private organization overrides the Member role that was granted me by #crdb-industries and #zed, a trivial and unbeatable privilege escalation vulnerability.

To fix this glaring mistake, I hastily added the words 'with subtractive interference' to the sentence right after it had been published:

An image of a GitHub commit diff, showing the addition
An image of a GitHub commit diff, showing the addition

But this fix created the exact same problem but in reverse! If I added one of my colleagues as a Member of #my-freelance-design-business, they would lose the Administrator role they have to their own organization's channels!

The fundamental problem here is that the security model we're using, Acess Control Lists, cannot be composed together in the ways we need to allow the kinds of behavior I described last week. Thankfully, there is an alternative model that could: Object Capabilities (OCaps). I've been excited about OCaps since I was introduced to the Spritely Institute and their Lisp-based Goblins framework. If you want to read more about Object Capabilities in-depth and learn some Lisp while you're at it, their Heart of Spritely paper goes into detail.

For our purposes, Object Capabilities boil down to a simple rule: access is authorization. If you have access to something, then you can do whatever that access allows you to do. Note where the record of the permissions designated resides, not on a person, not on a server but on the access itself. Now the problem I described above is trivially solvable. Instead of roles let's imagine actions that can be performed on a channel, Read and Write, that correspond to the Member and Admin roles above, and let's take away the cascade. Now my channels look like this:

- #my-freelance-design-business - (Write)
  - > #crdb-industries/#design  - (Read)
  - > #zed/#design              - (Read)

The problem has solved itself. I only have Read access to #crdb-industries/#design and my Write access to my personal channel is completely independent of its sub-channels. Hurrah! But this solution comes with one major downside: we don't think like this.

Zed is about putting user experience first. We strive to remove barriers between your will and the computer's actions. The human world is currently organized in terms of membership and roles. Whether or not that's a good thing, people think in terms like "I want to make Mikayla a Member of Zed", and we need to make it simple to express that. So why not just do both ACLs and OCaps?

If we're doing ACLs, we have to bring back cascading membership roles. It doesn't make sense (in our use case) to be a member of a channel and not be a member of its sub-channels. But to resolve the privilege escalation problem we can add a new kind of rule to each edge of the DAG, selected during edge creation: Inherits up to _. Let's redraw our example with this new rule, this time in FigJam because there's a lot more going on:

A diagram showing two root channels, #zed and #my-freelance-design-business, and two members in each channel: Nathan and Mikayla. In #zed, Nathan is an Admin, and Mikayla is a member. In #my-freelance-design-business, Nathan is a Member and Mikayla is an Admin. There is a third channel, called #design, with arrows connecting it to the other two channels. The arrow to #zed is tagged with Inherits up to Admin and the arrow to #my-freelance-design-business is tagged with Inherits up to Member
A diagram showing two root channels, #zed and #my-freelance-design-business, and two members in each channel: Nathan and Mikayla. In #zed, Nathan is an Admin, and Mikayla is a member. In #my-freelance-design-business, Nathan is a Member and Mikayla is an Admin. There is a third channel, called #design, with arrows connecting it to the other two channels. The arrow to #zed is tagged with Inherits up to Admin and the arrow to #my-freelance-design-business is tagged with Inherits up to Member

In this setup, the edge's inheritance rule acts like a filter on the cascading roles, rather than permission in and of itself (the pure OCap solution). This resolves the privilege escalation attack, as my Admin role on my own channel is transformed into a simple Member role when it travels along the #my-freelance-design-business -> #design edge. Tagging the #zed -> #design edge as Inherits up to Admin also solves the inverse problem of stealing my colleague's Admin access. There exists a path in the DAG where Nathan's Admin role hasn't been filtered away, so he is an Admin in #design. His membership in Mikayla's channel cannot interfere with his Admin role at Zed.

And even better, we can model safe privelege escalation with this system too. Let's say I became a core contributor to Rust Analyzer in my spare time and we had a channels setup like this:

A diagram showing two root channels, #zed and #rust-analyzer, and Mikayla's membership in each channel: In #zed Mikayla is a member. In #rust-analyzer, Mikayla is an Admin. Below #zed is another channel, #open-source, and below that channel another #zed-rust-analyzer which is tagged with Inherits up to Admin. There is also an edge between #rust-analyzer and #zed-rust-analyzer also with an Inherits up to Admin tag. Mikayla's membership role in #zed-rust-analyzer is shown to be Admin.
A diagram showing two root channels, #zed and #rust-analyzer, and Mikayla's membership in each channel: In #zed Mikayla is a member. In #rust-analyzer, Mikayla is an Admin. Below #zed is another channel, #open-source, and below that channel another #zed-rust-analyzer which is tagged with Inherits up to Admin. There is also an edge between #rust-analyzer and #zed-rust-analyzer also with an Inherits up to Admin tag. Mikayla's membership role in #zed-rust-analyzer is shown to be Admin.

Since Rust Analyzer and Zed co-administrate a channel, and I have an Admin role in #rust-analyzer, these rules allow my role to be upgraded to Admin in just #zed-rust-analyzer. Since membership cascades down, and never up, my Member role in all other #zed channels is maintained, just like it should be.

Now we can express the simple, human-oriented membership roles that we're all used to, without having to tradeoff flexibility and expressive power, and without opening trivial security holes. 🎉🎉🎉

Hopefully, we can start our internal testing of the Channels feature next week. I'm excited to see how it works in practice! Particular thanks to Quinn Wilton at Fission for working out this synthesis with me while we were talking through the permissions problem from last week.

Nathan

Exciting week for me.

Today I put together this proposal for plugins and shared it with Julia. Once we get the basics working, I'd love to do some macro foo to make it fairly easy to expose Rust objects as hosted objects in V8.

I've also made progress on making it more efficient to style Zed. Dynamic loading didn't work out super great, but I've settled on a playground that's really quick to compile, shortening the feedback on UI updates to a few seconds. We can do better eventually, but this loop is quite usable by comparison. A next step would be to allow the styles of our most common elements to be adjusted at runtime. Paired with a printed representation, we could potentially round-trip that to code fairly efficiently. This is all because we need Zed to be a place where designers write code. I think that's just more efficient for everyone.

Going forward, I want to try basing Zed's themes on the Rosé Pine series. This is their schema, but we replace their cutely-named colors with domain-specific, hue-neutral names:

pub struct ThemeColors {
    pub base: Range<Hsla>,
    pub surface: Range<Hsla>,
    pub overlay: Range<Hsla>,
    pub muted: Range<Hsla>,
    pub subtle: Range<Hsla>,
    pub text: Range<Hsla>,
    pub highlight_low: Range<Hsla>,
    pub highlight_med: Range<Hsla>,
    pub highlight_high: Range<Hsla>,
    pub success: Range<Hsla>,
    pub warning: Range<Hsla>,
    pub error: Range<Hsla>,
    pub inserted: Range<Hsla>,
    pub deleted: Range<Hsla>,
    pub modified: Range<Hsla>,
}

The ranges are two colors which can be sampled from 0.0 to 1.0, linearly interoplated in the HSL color space between the start and end of the given range.

The values I'm sampling with are a bit arbitrary at this point, but this code gives you the flavor. It's worth noting that components are generic over a type V which contains data that lives across multiple frames.

impl<V> Playground<V> {
    pub fn new() -> Self {
        Self(PhantomData)
    }
 
    pub fn render(&mut self, _: &mut V, _: &mut gpui::ViewContext<V>) -> impl Element<V> {
        workspace(&rose_pine::dawn())
    }
}
 
fn workspace<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
    column()
        .size(auto())
        .fill(theme.base(0.5))
        .text_color(theme.text(0.5))
        .child(title_bar(theme))
        .child(stage(theme))
        .child(status_bar(theme))
}
 
fn title_bar<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
    row()
        .fill(theme.base(0.2))
        .justify(0.)
        .width(auto())
        .child(text("Zed Playground"))
}
 
fn stage<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
    row().fill(theme.surface(0.9))
}
 
fn status_bar<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
    row().fill(theme.surface(0.1))
}

I'm planning to explore taffy as a layout model, so some of the syntax above could change to more strongly align with concepts from the web that it emulates. Familiarity is a good thing. We just need speed.