Tracked Capabilities for Safer Agents

*This post describes the ideas behind our paper “Securing Agents with Tracked Capabilities” and the tooling we are developing based on these ideas. The paper, written jointly with Yaoyu Zhao, Yichen Xu, Oliver Bracevac, and Nguyen Pham, received the best paper award at the inaugural ACM Conference on AI and Agentic Systems (CAIS 2026). *

A new class of risk

AI agents are changing how we build software and run business processes. The recipe is by now familiar: take a large language model, give it access to a set of tools, and let it pursue a goal on its own. The tools are made available through protocols such as MCP or through skills, and increasingly through code execution. Instead of calling one tool at a time, the agent writes a small program that calls the tools for it. This turns out to save a lot of tokens, and it is also believed to be safer. In our work we assume this style of tool use.

But there is a fundamental problem: an agent might use its tools inappropriately. For instance. it might delete data it should not touch, or leak private information to the outside world, or hack into a protected system. This can happen for a large number of reasons, such as an indirect prompt injection hidden in some document the agent reads, or a hallucination, or agent misalignment, or simply an ordinary mistake. The critical point is that the tool call usually happens automatically, with no human in the loop to catch problematic behavior.

This is a genuinely new kind of threat, and it comes precisely from the combination of the two technologies. Without tools, an LLM can only produce text or images, nothing that reaches out into the world. Without the LLM, we are back to ordinary software, with the ordinary defenses of code review, testing, and verification. Put the two together and you get AI-generated plans that invoke real tools with real effects, none of it vetted beforehand by a human. As agents become more capable, both what they can do for us and what they can do to us grows with every generation of models.

The usual answers are not enough. Model alignment can be broken, and it forces an uncomfortable trade-off between safety and usefulness. Sandboxing helps, but it needs fine-grained permissions that neither an untrusted agent nor a busy human operator can be trusted to get right. And asking a human to confirm every sensitive action leads quickly to confirmation fatigue, which is just another way of saying that the oversight stops working.

Don’t bet on the model

The central idea of our paper is this: agent safety should be a property of the infrastructure, not a bet on how the model behaves. We want to put the agent in a safety harness that permits a rich range of useful behavior while ruling out the dangerous parts, and we want the guarantee to hold no matter what the model does, even if the model is actively adversarial.

The tool we reach for is an old one from systems security: capabilities. In the object-capability model, first described by Dennis and Van Horn in 1966, an object can only act on another object if it holds an unforgeable reference (a capability) to it. No ambient authority, no acting at a distance. If you were never handed the key, you cannot open the door.

Object capabilities have traditionally been a purely runtime affair. Our contribution is to track capabilities in static types, building on a line of work on capture checking that has been developing in Scala over the last several years. This sounds like a small technical shift, but it fundamentally changes what is possible. Static tracking lets us express something that runtime checks cannot easily express: enforce that a particular object or function holds only capabilities in an allowed set. At the limit, this allows to enforce that the object or function is pure, i.e. that it holds no capabilities at all, and therefore cannot leak anything or affect anything outside itself.

Another reason for using types to track capabilities is good ergonomics. Types catch errors before the code ever runs. They impose no runtime overhead. They hold for all possible inputs, not just the ones you happened to test. And they are modular, so you can assemble a safe system out of reusable, separately-checked parts.

A concrete scenario

Suppose we ask an agent to compare a set of contracts and write a short summary of what changed between them. The contracts are confidential. The agent must produce its summary, but it must not be able to reveal the contents anywhere else: not to another tool, not by sending them over the network, and not by quietly carrying them over into its answer to some later, unrelated prompt. On top of that, the language of the contracts should not be allowed to steer the agent’s behavior, so that a prompt injection buried in a contract cannot take over. Until recently, a requirement like this was considered very hard to satisfy with agent technology.

Here is how it works in our design. We hand the agent four tools: read access to the document database, write access to a single output file for the summary, a diff tool, and a pure summarizing LLM. Any confidential document comes back wrapped in a Classified wrapper that looks roughly like this:

class Classified[T]: def reveal(using CanAccess[T]): T // only for authorized holders def transform[U](f: T -> U): Classified[U] // only via a pure function def aggregate[U](x: Classified[U]): Classified[(T, U)]
The agent is never given the CanAccess capability, so it can never call reveal. What it can do is transform the classified content, but only through a function of type T -> U. The arrow is the whole point. In our notation, T -> U is a pure function: one that captures no capabilities. It can look at the confidential data, but because it holds nothing that reaches the outside world, it has nowhere to leak the data to. A function that can perform side effects would be written with a different arrow, T => U, and that arrow is exactly what the type system forbids here.

So the agent writes a program that reads the documents, diffs them, and calls the pure LLM to summarize, and finally writes the summary to the output file. The summarization is constrained by the types to be a pure computation without any side effect. The original, hard question was: how do we know, without reading the agent’s code, that it does these things and nothing else? The answer is that we don’t have to read the code. The compiler does. If the agent’s program tried to smuggle the classified content out through any capability, it simply would not type-check, and it would never run. In our experiments, this gave us zero leakage, even against models deliberately prompted to exfiltrate the data.

The scenario highlights an important difference where tracked capabilities add genuinely new expressive power. For traditional capabilities, once you passed a capability to an agent, you cannot force it to give it up again, or to disable its use for a defined part of its computation. In that sense, traditional capabilities are a single line defense. In our example, if the agent as a whole is given the capability to communicate with other sites on the internet we cannot prevent it under the traditional capability model from doing this while it reads the classified data. By contrast, tracked capabilities enable a defense in depth, where critical accesses to data can have special, stronger protections than the rest of the system. In our scenario, the second line of defense is the pure function arrow for the parameter of the transform function, which forces the agent to ignore all capabilities it holds while accessing the classified data.

Why types, and not yet another policy language

There are other ways to attack this problem, so it is worth being clear about why we think ours is better.

One family of approaches keeps the ordinary, unchecked tool interface and bolts a separate access-control language on the side. Amazon’s Bedrock AgentCore with the Cedar policy language is a good example. This is workable and it may well become part of the safety infrastructure at large cloud providers. But it means describing the same interaction in two languages that have to be kept in sync, and a standalone policy language can usually do little more than enforce access lists. Our scenario needs more than that: we need to say that one sub-computation — the summary — is side-effect free, while the agent as a whole is allowed to have effects. Capabilities in types express this naturally; an access list cannot.

Another approach, taken by systems such as CaMeL, does information-flow control at runtime, using a special interpreter and a pair of data- and control-flow analyses. This is closer in spirit to what we want. But it needs a dedicated DSL and runtime, several analyses that have to cooperate, and a two-stage process in which a trusted model first produces a whole program that is then analyzed. Because our checks live in the type system of an ordinary language, the agent can generate small code snippets on the fly, as the results of real-time deliberation, and each one is checked as it arrives. That is a more flexible execution model, and it comes essentially for free once the language already tracks capabilities.

This is the deeper reason we like the type-based route. We are not inventing a new permission language - permission languages are notoriously hard to design and harder to use. We are taking capabilities, a well-understood idea from operating systems, and folding them into the type system of a mature, general-purpose language. The notation stays lightweight and readable, which matters, because humans still have to write the interfaces, review the contexts an agent is given, and occasionally debug the code an agent produces.

What are the costs?

There are several requirements for constructing safe agent harnesses in the way we envision them. First, we need a language that is genuinely capability-safe. Capabilities must be unforgeable, and capability requirements must be impossible to “forget”. Capabilities must guard all effects that are relevant for system safety and that cannot be contained at runtime. Scala 3 can express tracked capabilities through its capture-checking feature, and the system has matured to a stage where many interesting effects, including finegrained mutable state changes can be controlled. But the full language also has escape hatches — unchecked casts, reflection, and so on — that would let a determined agent slip out of the harness. So we define a safe subset, selected with a simple language import, that switches these off and turns on capture checking. This will be the topic of a future article.

While agent-generated code runs in this safe subset, the tools it calls can be written in regular Scala, and indeed in any language that runs on a platform Scala targets, as long as they implement the capability interfaces honestly. For instance, agent infrastructure could be written in Java or Javascript/Typescript, or any language targeting WASM, since there are Scala implementations for these platforms. So the applicability of our techniques is not limited to Scala teams. Code snippets for tool invocations need to be written by agents in safe Scala (and we found them perfectly capable of doing this), whereas the system as a whole can be written in many other languages.

Looking at the specific development costs to set up a harness, we can distinguish two parts. First, there’s the investment in the reusable infrastructure, consisting in our scenario of the Classified wrapper, the safe document database, and the diff and summarize tools. This requires some effort, but it needs to be paid only once and can then be reused for other agents. The other part, specific to each request, is the environment that says “here are exactly the capabilities this agent gets”. This part is cheap to write and easy for a human to review before the agent is ever invoked. That asymmetry is what makes us think the approach is practical and not just elegant.

Current status

We built a working system — the TACIT agent harness — on these ideas, and evaluated it on the standard benchmarks τ-bench and SWE-bench Lite across several frontier models. The key finding is that we got complete prevention of leakage attacks, while keeping the agent’s usefulness on ordinary tasks intact. Safety did not come at the price of utility.

There’s ongoing work to make this available for general use in practice. Tracked capabilities are supported in standard Scala 3 under a language import, see the doc pages for capture checking for details. The TACIT agent harness uses this technology to prevent information leakage and other safety violations. Also worth mentioning is genscalator, a suite of well-typed tools intended for coding agents as a replacement for unrestricted bash access. We believe that practical deployments of safe agent harnesses can arise from a combination of these tools and techniques.

Why this matters now

The case for defending agents at the level of the infrastructure, rather than trying to make every model perfectly aligned, is by now fairly well established. What has been missing is a rigorous, usable way to actually do it. Our bet is that tracking capabilities in types is that missing piece. If we get it right, the safety of an agentic system stops being a hope about model behavior and becomes a property you can point to, prove, and rely on, the way we already rely on memory safety or type safety today.