Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
156690 stories
·
33 followers

What's new in Astro - June 2026

1 Share
June 2026 - Astro 7, summer swag collection, Astro Germany meetup, and more!
Read the whole story
alvinashcraft
43 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Beyond Prompt Injection

1 Share

In late 2025, the security community stopped treating indirect prompt injection as a theoretical risk. It had spent two years as a tidy lab demonstration; then production systems started getting hit. The OWASP Top 10 for LLM applications now ranks prompt injection as the number-one risk, NIST has called indirect injection generative AI’s greatest security flaw, and academic researchers showed that a single poisoned email could coerce a model into exfiltrating SSH keys in up to 80% of trials, with zero user interaction. The attack needs no malicious binary, no phishing clicks, and no anomalous login. The agent simply reads content and takes action, exactly as designed, and the content was written by an attacker.

The most instructive example is ForcedLeak. In September 2025, researchers at Noma disclosed a critical vulnerability chain (CVSS 9.4) in Salesforce’s Agentforce platform: An attacker embedded malicious instructions in the description field of a routine Web-to-Lead form. The text sat harmlessly in the CRM until an employee later asked the AI agent to process that lead, at which point the agent dutifully executed both the legitimate query and the attacker’s hidden payload, exfiltrating sensitive CRM data to an external server. The detail that should keep you up at night is that the exfiltration destination was a domain still on Salesforce’s trusted allowlist, one that had expired and which the researchers re-registered for about five dollars. Every security control saw legitimate traffic to a trusted domain. Nothing looked wrong.

If your instinct reading that is “we filter for prompt injection,” you’re defending the wrong perimeter. Input filtering is necessary but nowhere near sufficient. The uncomfortable truth is that the injection isn’t the breach; the action is. And almost everything we call “AI security” is aimed at the wrong half of that sentence.

The defense everyone is building

Ask most enterprise AI teams how they secure their agents, and you’ll hear a consistent answer: They sanitize inputs. They harden system prompts with elaborate instructions to ignore conflicting directives. They run classifiers over incoming content to flag adversarial patterns. Some have adopted the more sophisticated training-time defenses the frontier labs have published—instruction hierarchies that teach a model to assign differential trust to different sources and reinforcement-learning approaches that harden models against injection in agentic contexts.

All of this is good work, and none of it should be abandoned. But notice what every one of these techniques shares. They all try to stop the model from being fooled. They assume that if we make the model robust enough at the input layer, the system is safe. That assumption is the vulnerability.

We’ve spent two years trying to make the model unfoolable. The systems that survive contact with production assume it will be fooled anyway.

Why the input layer is the wrong perimeter

Prompt injection isn’t a bug a future model will lack. It’s a structural property of how language models work. The model consumes a single undifferentiated stream of tokens at the moment of inference. Your instructions, the retrieved document, the tool output, and the web page just fetched are indistinguishable channels collapsed into one context. There’s no hardware-enforced boundary between “trusted instruction” and “untrusted data” the way there is between kernel space and user space in an operating system.

This is why the attack surface explodes the moment an agent becomes agentic. A chatbot that only talks is a contained risk. An agent that retrieves from the open web, reads email, queries databases, and calls APIs ingests adversarial content from a dozen sources on every turn, and any one of them can carry an instruction. Researchers cataloging real agent ecosystems have already found hundreds of malicious third-party extensions performing data exfiltration and silent injection without any user awareness. These aren’t laboratory curiosities. They’re the production environment.

So, if you can’t guarantee the model will never be fooled—and you can’t—then architecture that depends on it never being fooled is built on sand. You need a second principle, one distributed systems engineers have understood for decades.

Verify, then trust

The principle is simple to state and hard to retrofit: An agent’s proposed action should be validated against an external, deterministic policy before it executes, regardless of why the agent proposed it. The validator doesn’t ask whether the instruction that produced the action was legitimate. It doesn’t try to detect the injection. It asks a different and far more answerable question: Is this action, on its face, permitted?

This inverts the burden. Detecting a cleverly disguised malicious instruction is open-ended because the adversary gets to be arbitrarily creative. Checking whether a wire transfer exceeds a hard dollar limit is a closed problem with a definite answer. We move the security decision from where the attacker has infinite freedom to where they have almost none.

Crucially, the check must be deterministic code, not another model asking, “Does this look dangerous?” The moment you ask a second LLM to adjudicate, you’ve reintroduced the exact same vulnerability one layer down. The enforcement layer is boring, auditable conventional software, and that’s the point.

Here’s what it looks like in practice. An agent managing procurement proposes an action, and a runtime contract evaluates it before anything reaches a real API:

# agent_contract.yaml
 agent_id: "procurement_executor_07"
 role: "EXECUTOR"
 policy:
   approve_invoice:
 	max_amount_usd: 50000
 	allowed_vendors: from_approved_registry
 	require_human_above_usd: 10000

 # Runtime, on a proposed action:
 ACTION   approve_invoice(vendor='Acme', amount=1200000)
 REJECTED policy violation: max_amount_usd
      	proposed 1,200,000 / limit 50,000
      	action discarded, human notified, no API call made

The injected instruction at 2:14am never matters here. The agent can be perfectly, catastrophically fooled, and the wire transfer still doesn’t happen, all because a simple deterministic check stood between the model’s output and the outside world, and the proposed action failed it.

This only works if the action arrives structured, which makes structure a precondition.

The contract inspects approve_invoice (vendor, amount) cleanly only because the action is already typed. If the agent emits prose, “please approve the Acme invoice,” something has to parse it, and the only thing that parses open language is another LLM, so the indeterminacy walks back in. That dictates the design.

A consequential action must cross the boundary as a typed tool call, never as free text. Where the input is unavoidably natural—an email saying, “Wire them their balance” for example—let the model extract a structured value but never let its extraction be self-authorizing. The model proposes the amount; the gate still checks it against the limit, the vendor registry, and the actual balance in the system of record, not the number the email asserted. Extraction is probabilistic, while validation stays deterministic.

A few decisions are pure judgment with no schema, such as “Is this email phishing?” There the model stays in the loop. You bound the consequences instead, with reversibility and human review above a threshold. Contracts protect parameterizable actions, and unparameterizable judgments fall back to containment.

The architecture this implies

Once you accept that the action layer is where security lives, three design commitments follow, and they map almost directly onto principles that hardened distributed systems years ago.

Least privilege for agents, scoped to the action, not the agent. The naive version assumes you can predict what an agent will do and provision it accordingly. For a specialized agent you can: One that only summarizes has no business holding a credential that moves money. But the agents people actually reach for are general. In a single session, I might ask a coding agent to summarize a file, write code, execute it, and query company data—four tasks with four risk profiles, none of which are enumerated in advance. Static least privilege collapses the moment one identity spans that range.

The fix is to make privilege a property of the action, not the agent. The agent holds no dangerous capability by standing grant; it requests narrow, transient elevation per action, which the same deterministic gate approves or denies. Reading a document is auto-approved; querying the warehouse is not. The dangerous credential exists only for the instant the action is permitted, then evaporates. One caveat: This governs what an agent may reach but not what the code it writes then does. Executing code can be gated as a capability, but what executes still needs containment, sandboxing, and egress control, because generativity is a different problem from access.

Zero trust for machine identities. Every action an agent takes should be authenticated and authorized as if it came from an untrusted actor, because, functionally, it might be acting on an attacker’s instructions. The proliferation of agents has expanded the attack surface faster than most identity systems were designed to handle, and treating agent traffic as inherently trusted because it originates inside your own system is precisely the mistake.

Capability contracts at the boundary. Every consequential action passes through a deterministic gate that encodes what is allowed, dollar limits, rate limits, allowlisted destinations, mandatory human review thresholds. The contract is version-controlled, auditable, and lives entirely outside the model.

The trap of normalized deviance

The quieter organizational danger is the slow accumulation of false confidence from connecting insecure agents to real systems and watching nothing bad happen. . .for a while. Researchers have warned about indirect injections for years, but most deployments have gotten away with it. Each uneventful day makes the next risky connection feel safer. This is the normalization of deviance. Every system that eventually failed catastrophically felt the same way: fine, fine, fine, until it wasn’t.

The teams that will weather the coming wave of agent incidents aren’t the ones with the cleverest input filters. They’re the ones who assumed compromise from the start and built the boring enforcement layer anyway, the ones who decided that an agent’s autonomy ends precisely at the point where it tries to do something irreversible.

Where to start on Monday

You don’t need to rearchitect everything. Start by inventorying the actions your agents can take, and sort them by blast radius: What’s the worst thing that happens if this action fires when it shouldn’t? For every high-blast-radius action, write a deterministic contract that gates it and put a human in the loop above a threshold you can defend to your risk team. Then, and only then, keep hardening your inputs.

Prompt injection won’t be solved at the input layer, because it can’t be. But it can be rendered survivable at the action layer, where deterministic code gets the final word. The model’s job is to be useful. Your architecture’s job is to make sure that when the model fails—or worse, when it has been turned against you—the failure stops at the gate.



Read the whole story
alvinashcraft
44 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

The Anti-Pattern Bingo Team—When Success Is a Zero-Sum Game | Gunnar Fischer

1 Share

Gunnar Fischer: The Anti-Pattern Bingo Team—When Success Is a Zero-Sum Game

Read the full Show Notes and search through the world's largest audio library on Agile and Scrum directly on the Scrum Master Toolbox Podcast website: http://bit.ly/SMTP_ShowNotes.

 

"This was neither Scrum, nor a team. It was more like the anti-pattern bingo team." - Gunnar Fischer

 

When Gunnar took his first job abroad, he walked straight into what he calls the "anti-pattern bingo team"—a supposed Scrum team that was neither Scrum nor a team. The company was in trouble, the team had a bad reputation and was used as a punching bag, the manager-of-manager treated them like a stepchild, and the new manager didn't seem to know what he had signed up for. The team members themselves didn't really want to work together. The goals slipped. Decisions were made in back rooms, outside the official meetings. And underneath it all sat the most corrosive belief Gunnar names: that success is a zero-sum game—if you win, I lose. With that mindset, there is no team, just individuals defending turf. One pattern stuck with him so clearly he gave it a name: sandcastle planning. The team would finish a Sprint planning, agree on a goal, and the manager would walk in right after the meeting and overturn the whole thing with his own priorities. Over time, the team stopped putting effort into planning. Why build the castle if someone will trample it? Even worse, when an escalation finally surfaced, Gunnar—an immigrant—was told that as a German, he must be "very authoritarian." A label, served up as analysis. That was the moment he knew the team would never have safe disagreements, never reach the right level of challenge, never recover.

 

In this segment, we talk about scrum master anti-patterns, the corrosive effect of treating people as labels, and how the absence of an explicit reason for the team's existence makes everything else collapse.

 

Self-reflection Question: Does your team have a clear, explicit reason for existing—or are you just a group that shares a technology, a building, or a reporting line?

Featured Book of the Week: Scrum Mastery (2nd Edition) by Geoff Watts

For Gunnar, the book that shaped him most as a Scrum Master is Scrum Mastery (2nd Edition) by Geoff Watts—what he calls "the noble knight of Scrum books." He first read it just after a Scrum course and thought, "What should I even do with this kind of wisdom?" Years later he came back to it and understood. A few years after that, he became modest about it: these are truths, but it's about making them true—and watching for when they aren't. The book is full of phrases like "a good Scrum Master is indispensable; a great Scrum Master is dispensable and wanted." That last line captured it perfectly for him: success isn't being unneeded, it's being chosen. As Gunnar puts it: "In times when everybody is challenged and people are making fun of agile practitioners, this brings back all of the ideals of what a Scrum Master is really about." You can also listen to our previous episodes with Geoff Watts on the podcast.

 

[The Scrum Master Toolbox Podcast Recommends]

🔥In the ruthless world of fintech, success isn't just about innovation—it's about coaching!🔥

Angela thought she was just there to coach a team. But now, she's caught in the middle of a corporate espionage drama that could make or break the future of digital banking. Can she help the team regain their mojo and outwit their rivals, or will the competition crush their ambitions? As alliances shift and the pressure builds, one thing becomes clear: this isn't just about the product—it's about the people.

 

🚨 Will Angela's coaching be enough? Find out in Shift: From Product to People—the gripping story of high-stakes innovation and corporate intrigue.

 

Buy Now on Amazon

 

[The Scrum Master Toolbox Podcast Recommends]

 

About Gunnar Fischer

 

Gunnar is the leader of the Chocolate Guild. Agile practitioner with a software developer background and a strong interest in people, intercultural contacts and the bigger picture. Gunnar's purpose is to teach and to learn, to grow as a person and to support others who want the same.

 

You can link with Gunnar Fischer on LinkedIn.

 

You can also read Gunnar's writing on his blog, Leader of the Chocolate Guild.





Download audio: https://traffic.libsyn.com/secure/scrummastertoolbox/20260630_Gunnar_Fischer_Tue.mp3?dest-id=246429
Read the whole story
alvinashcraft
44 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Closed class hierarchies: Exploring the .NET 11 preview - Part 4

1 Share

In this post I look at the implementation of closed class hierarchies that is available in .NET 11 preview 5. I'll describe what a close closed hierarchy is, how to create one, and discuss why you might want to.

What is a "closed class hierarchy"?

A closed class hierarchy is a class hierarchy that can only be defined within a single assembly. Attempting to derived from a closed class from a different assembly is a compilation error. This is easiest to see in action.

Imagine you have the following classes, all in the same assembly:

// Create a closed base class
public closed class Animal { }

// Each class derives from the closed Animal class
public class Dog : Animal { }
public class Cat : Animal { }
public class Horse : Animal { }

The Dog, Cat, and Horse classes all derive from Animal. This is all C# 101; the closed keyword isn't really changing anything about that.

The difference is if we create a new assembly, and try to derive from Animal in a different assembly:

// Assembly 2
public class Cow : Animal { }

then this won't compile! Instead you'll get an error like the following:

error CS9382: 'Cow': cannot use a closed type 'Animal' from another assembly as a base type.

So from this example, you can see that this approach allows you to have a public base type, but ensure that no one other than you can define types derived from it. This can be very useful for modelling certain domains, and can simplify the logic in your own code significantly. However, that's not all they're for.

Couldn't we do this already with private constructors?

Being able to prevent derived types using closed is useful, but there have been other ways to achieve this in the past. By providing a private protected or internal default constructor on the base class, it's practically impossible to create a derived type.

For example, if we update the Animal definition to:

// Assembly 1
// remove 'closed', make abstract, and add constructor
public abstract class Animal
{
    private protected Animal()
    {
    }
}

then you still can't define Cow in another assembly:

// Assembly 2
public class Cow : Animal { } // Won't compile

because you get one of two different errors:

error CS0122: 'Animal.Animal()' is inaccessible due to its protection level
error CS1729: 'Animal' does not contain a constructor that takes 0 arguments

Using closed is clearly semantically nicer than the private constructor approach, but it doesn't seem like it's actually providing new functionality.

However, there is a difference. With the private constructor approach, the compiler didn't really "know" that you couldn't create any derived types, because theoretically you could, even if you couldn't practically.

With the closed keyword, when the compiler builds an assembly, it knows exactly what all the derived types of a closed class are. The main benefit of that is that the compiler can apply exhaustiveness checking to switch expressions.

Exhaustiveness checking in switch expressions

Let's say we have a "normal" class hierarchy, similar to the one shown above, but this time just using abstract instead of closed:

// Abstract base type
public abstract class Animal { }

// The same derived types as before
public class Dog : Animal { }
public class Cat : Animal { }
public class Horse : Animal { }

We then also define the following method, which takes an Animal instance, and performs a switch:

static string Speak(Animal animal) => animal switch
{
    Dog => "Woof",
    Cat => "Meow",
    Horse => "Neigh",
};

If you compile this code, you'll get a warning:

warning CS8509: The switch expression does not handle all possible
    values of its input type (it is not exhaustive). For example,
    the pattern '_' is not covered.

Even though you're handling all the types that could be passed to Speak(), the compiler doesn't know that. As far as it's concerned, you could have created a type derived from Animal in a different assembly, and passed it to the method.

Note that even if we add the private protected constructor here, so that practically a different assembly can't derive from the type, that doesn't change the warning. The compiler can't infer the fact that there will only ever be these three implementations.

Using closed means that the compiler does know that these are all of the implementations, and there can't be any more. That means the compiler can correctly apply exhaustiveness checking to switch expressions which used closed types.

This might seem like a small thing, but it's actually quite a big deal for writing correct code.

For example, imagine you have code similar to the previous that you were using with .NET 10. In order to quiet the warnings, you add a catch-all handler to your switch expressions

static string Speak(Animal animal) => animal switch
{
    Dog => "Woof",
    Cat => "Meow",
    Horse => "Neigh",
    _ => throw new InvalidOperationException(); // Can't be hit
};

This keeps the compiler satisfied, but it's a little bit ugly. But the important thing is what happens when you introduce a new type to the project:

public class Hamster : Animal {}

The big problem here is that all your existing code continues to compile. Which means Speak is going to throw an InvalidOperationException() when it's called 😬 In this way, the lack of correct exhaustiveness checking for our type hierarchy has made our switch expression fragile to changes.

Note that this is a very "functional programming" approach, in which the data (the Animal classes) are separate from the methods (Speak()). This is in contrast to an object oriented approach, in which Speak would likely be defined on the Animal types themselves. These are two different approaches, and both are valid and useful in different situations.

With the closed keyword, you no longer need the catch-all parameter:

// Animal is closed
public closed class Animal { }

static string Speak(Animal animal) => animal switch
{
    Dog => "Woof",
    Cat => "Meow",
    Horse => "Neigh",
    // We no longer need a fallback clause
};

That means that when we add our new implementation, Hamster, the compiler will emit a warning, telling us everywhere that needs to be updated!

warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'Hamster' is not covered.

All in all, this change means modifying a closed hierarchy is generally safer than a standard class hierarchy, because the compiler is able to catch unhandled cases much more easily.

How do closed class hierarchies relate to unions?

In a previous post, I described the union support that is also coming to C# with .NET 11. One of the advantages of the union support is that you get similar exhaustiveness checking in switch expressions, for example:

// Three different cases
public record Windows(string Version);
public record Linux(string Distro, string Version);
public record MacOS(string Name, int Version);

// Combined in a union, which can represent one of them
public union SupportedOS(Windows, Linux, MacOS);

// Use the union in a switch expressions
string GetDescription(SupportedOS os) => os switch
{
    Windows windows => $"Windows {windows.Version}",
    Linux linux => $"{linux.Distro} {linux.Version}",
    MacOS macOS => $"MacOS {macOS.Name} ({macOS.Version})",
}; // note: no catch-all _ required

Just as for the closed class hierarchy, the union supports exhaustiveness checking so you don't need the catch-all parameters. However, the union support is conceptually different. There's no hierarchy involved with the union; rather it defines the specific possible values, and the compiler makes sure you handle all of those cases in the switch expression.

Using closed class hierarchies in .NET 11

I've talked a lot about what closed class hierarchies are for, and why they're useful, but we haven't looked at how to actually enable them in your project.

First, closed class hierarchies were introduced in .NET 11 preview 5, so you must install the .NET 11 preview 5 SDK or higher.

Note that depending on your setup, you may need to add a global.json file and set allowPrerelease: true.

Once you've installed the SDK, if you try to use the closed keyword in a .NET 11 project, you'll get the following warnings:

error CS8652: The feature 'closed classes' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version.
error CS0656: Missing compiler required member 'System.Runtime.CompilerServices.ClosedAttribute..ctor'

To fix these errors, you need to do two things:

  • Add <LangVersion>preview</LangVersion> to your csproj
  • Manually define the [Closed] attribute

The first of these requires opening your .csproj file, and adding the <LangVersion> property to a <PropertyGroup>:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net11.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>

    <!-- Add this 👇 -->
    <LangVersion>preview</LangVersion>
  </PropertyGroup>

</Project>

The second requirement, adding the [Closed] attribute, is a temporary issue; it likely won't be necessary in preview 6 or later, as the runtime will provide the attribute directly. For now, you should add the following to your project:

namespace System.Runtime.CompilerServices;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
internal sealed class ClosedAttribute : Attribute { }

Once you've done all these steps, you should be able to use closed hierarchies in your project.

What are the limitations and features of closed class hierarchies?

When you apply the closed keyword to a class or a record, it implicitly makes the class abstract, so you can't use the sealed or static modifiers on the type. Additionally, you can't explicitly use abstract on the type either, even though it is abstract 😅

// Valid definitions
public closed class Animal { }
public closed record Animal { }

// Invalid definitions
public closed abstract class Animal { } // error CS9384: a closed type cannot be marked abstract because it is always implicitly abstract.
public closed sealed record Animal { } // error CS9381: a closed type cannot be sealed or static
public closed static record Animal { } // error CS9381: a closed type cannot be sealed or static

When you derive from a closed type, the derived classes themselves aren't automatically closed, but it's valid to mark them as such:

public closed record Animal { }

// Dog is not automatically closed, but _can_ be made closed
public closed record Dog : Animal{ }
public record Labrador : Dog { }
public record Collie : Dog { }

You can have generic closed types, but if a derived class is also generic, then all of its type parameters must be used in the base class specification:

public closed class Animal<T> { }
class Dog<U> : Animal<U> { }   // Ok, because 'U' is used in base class
class Cat<V> : Animal<V[]> { } // Ok, because 'V' is used in base class
class Horse<W> : Animal<int> { } // error CS9383: 'Horse<W>': The type parameter 'W' must be referenced in the base type 'Animal3<int>' because the base type is closed.

Obviously the main restriction on closed hierarchies, is that the whole hierarchy must be defined in a single assembly. There is one additional subtle restriction that comes up if you apply the sealed keyword to your derived types.

Making a closed hierarchy a sealed hierarchy

If you mark all the derived types of a closed class as sealed, then it is said to have a sealed hierarchy. In these cases, the compiler is able to make even stronger assumptions about your code, and so can add errors for scenarios that indicate something is probably wrong in your code.

For example, let's stick with the same familiar hierarchy, but this time we've marked all of our derived Animal types as sealed:

public closed class Animal { }

// All derived types are sealed
public sealed class Dog : Animal { }
public sealed class Cat : Animal { }
public sealed class Horse : Animal { }

Let's also imagine we have the simple IPet interface:

public interface IPet { }

Now, if we have some code that tries to explicitly cast from an Animal instance to IPet, the compiler can "see" all of the derived types, can see that none of them implement IPet, and therefore can reason at compile-time that this conversion will fail:

Animal animal = GetSomeAnimal();
var pet = (IPet)animal; // error CS0030: Cannot convert type 'Animal' to 'IPet'

This turns a guaranteed runtime error into a compile-time error. Lovely!

How are closed hierarchies implemented?

If, like me, you're wondering how closed hierarchies are implemented under the hood, it's actually pretty simple. Taking our Animal example again:

public closed class Animal { }

The compiler generates a type that looks like this:

[Closed] // Marks the type as a closed type
public class Animal
{
    [CompilerFeatureRequired("ClosedClasses")] // Tells the compiler that it needs this feature
    public Animal() { }
}

The [Closed] attribute added to the type tells downstream consumers of the assembly that the type is closed, and is not allowed to be derived from. That's why we had to add that attribute to use the feature in preview 5; because the compiler was emitting code that referenced it.

The [CompilerFeatureRequired] attribute is an interesting way of making sure that you can't bypass the closed restriction by using a version of the .NET SDK that doesn't understand the closed hierarchies feature.

For example, imagine you've built a library using the .NET 11 SDK. You target net8.0, so that your library can be used across all supported versions of .NET Core, but then you hand this assembly off to another team. The other team is using the .NET 8 SDK - if they try to derive from the Animal type, their compiler blocks it even though it doesn't know about or understand the [Closed] attribute. The .NET 8 SDK sees that the "ClosedClasses" feature is required (as per the [CompilerFeatureRequired] attribute), and blocks you from deriving from them.

Just to be clear, this only blocks using the closed feature with old SDKs. You're perfectly able to build for earlier runtimes, as long as you're using the .NET 11 SDK, and have set the <LangVersion> as required.

Note that if you are targeting earlier runtimes, you may need to add additional attributes to your project, which are only defined in later versions of the runtime. Alternatively, you can use a pollyfill library like Simon Cropp's Polyfill to handle all that for you automatically!

That brings us to the end of this look at closed hierarchies. In some ways this is a very simple feature, but I'm personally looking forward to using it to accurately and safely model the expectations of libraries I'm writing, as well as to benefit from more exhaustiveness checking!

Summary

In this post I described the closed class hierarchy feature. A closed hierarchy is one in which all of the derived types are contained in a single assembly. Marking a class or record as closed implicitly makes it abstract, and means no external assemblies can derive from it. Closed hierarchies also benefit from additional exhaustiveness checking in switch expressions.

Read the whole story
alvinashcraft
44 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Building Persistent Page Transitions with WebGPU and Vanilla JavaScript

1 Share
A step-by-step tutorial showing how to build seamless GPU-powered page transitions by combining a persistent WebGPU scene, DOM tracking, and a lightweight vanilla JavaScript router.



Download video: https://codrops-1f606.kxcdn.com/codrops/wp-content/uploads/2026/06/tutorial-video-5mb.mp4?x68649
Read the whole story
alvinashcraft
1 hour ago
reply
Pennsylvania, USA
Share this story
Delete

GitHub Copilot SDK Deep Dive: Session Memory

1 Share

The GitHub Copilot SDK just shipped a new feature: optional memory configuration on session create and resume. Here is what it does, and how it is different from persisted sessions.

The wrong mental model first

When I heared "session memory" my first thought was "persisted sessions" — the ResumeSessionAsync flow that lets you reload an existing session by ID and continue where you left off. That is not what this is. Persisted sessions are about durability of the conversation itself: close the app, reopen it, pick up the thread. Memory configuration is something different.

What memory configuration actually does

Memory is a feature of the Copilot runtime that lets the agent read and write facts across turns — a kind of long-running knowledge store that the agent can consult and update during a session. Think of it as the agent's notepad, not the conversation log.

The new MemoryConfiguration type exposes a single Enabled flag today. You opt in per session, on both create and resume:

var session = await client.CreateSessionAsync(new SessionConfig
{
    Model = "gpt-4o",
    Memory = new MemoryConfiguration { Enabled = true }
});

That's it. The SDK serialises it as { "memory": { "enabled": true } } on the wire. When you leave Memory unset, the field is omitted entirely and the runtime applies its own default.

Now we can ask our agent to remember stuff:

The mode-based default matters

There is a subtle default you need to understand. The SDK has two client modes:

  • CopilotClientMode.CopilotCli (the default): Memory is left unset. The runtime decides.
  • CopilotClientMode.Empty: Memory defaults to disabled unless you explicitly set it.

A caller-supplied value always wins in either mode. If you are building an app on Empty mode and you want memory, you must opt in explicitly. The runtime will not silently enable it for you.

Persisted sessions vs. memory: the actual difference

  Persisted sessions Memory configuration
What it stores The full conversation thread Facts/notes the agent writes
Lifetime Until you delete the session Managed by the runtime
API surface ResumeSessionAsync(sessionId) SessionConfig.Memory
Scope Cross-process, cross-restart Within the session, agent-controlled

A persisted session lets you reload the conversation. Memory lets the agent remember things across turns within — and potentially across — sessions, depending on how the runtime implements it.

You can use both at the same time: resume a persisted session with memory enabled, and the agent brings its notepad along.

Resuming with memory

ResumeSessionAsync also accepts the memory configuration:

var session = await client.ResumeSessionAsync(sessionId, new ResumeSessionConfig
{
    Memory = new MemoryConfiguration { Enabled = true }
});

This means you can control memory independently of whether the session is new or resumed. Useful if you want to enable memory for an existing session that was originally created without it.

More information

PR #1617 — SDK: add optional memory configuration to session create and resume

GitHub Copilot SDK Deep Dive: CopilotClientMode

Read the whole story
alvinashcraft
1 hour ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories