Defining your own capability
Libraries declare their own capability types and the discipline applies uniformly. A function that takes a SendEmail can send mail; nothing else can.
The motivation
The seven built-in capabilities cover IO. They do not cover everything you might want to express as a capability. A library that sends email through SMTP, talks to a database, publishes to a message queue, or signs JWTs has a real authority surface that the type system today only sees as "Net + maybe Fs". From a caller's point of view that is too coarse.
Capa lets a library define a new capability that is just as real as Net or Fs. The discipline applies the same way: any function that wants to send email has to receive a SendEmail value as a parameter.
The declaration
A user-defined capability looks like a trait, but the keyword is capability:
capability SendEmail
fun send(self,
to: String,
subject: String,
body: String) -> Result<Unit, IoError>
The declaration is the contract. Anyone who implements SendEmail must provide send with this exact signature; anyone who receives a SendEmail value can call send on it without knowing the concrete implementation.
The analyzer treats SendEmail as a capability: it is subject to the same no-aliasing, must-be-used rules as the built-ins.
An implementor
A concrete implementor is a struct paired with an impl SendEmail for ThatStruct:
type SmtpMailer {
server: String,
net: Net
}
impl SendEmail for SmtpMailer
fun send(self,
to: String,
subject: String,
body: String) -> Result<Unit, IoError>
let _ = self.net.post("smtp://${self.server}/", body)?
return Ok(())
Two things are unusual about this struct. First, SmtpMailer has a Net field. Built-in capabilities cannot normally appear in struct fields; that would be a way to smuggle authority sideways. The rule relaxes for cap-bearing structs: a struct that implements a user-defined capability is allowed to hold built-in caps as fields, on the condition that it is itself treated as a capability by the discipline.
Second, the constructor is just a regular function. A built-in cap cannot be returned from a function; a user-defined one can. That is the entry point into a user-defined cap: a factory function that takes whatever built-in caps it needs and returns a fresh implementor.
fun make_mailer(net: Net, server: String) -> SmtpMailer
return SmtpMailer { server: server, net: net }
Inside make_mailer, the built-in net flows into the struct field. After construction, SmtpMailer takes over the discipline: a function that holds a SmtpMailer can send email; one that does not, cannot.
Using it
From a caller's perspective, the user-defined cap behaves exactly like a built-in:
fun notify_user(mailer: SendEmail, user_id: String) -> Result<Unit, IoError>
mailer.send("${user_id}@example.com", "Welcome", "Hello")?
return Ok(())
fun main(net: Net, stdio: Stdio)
let mailer = make_mailer(net, "smtp.example.com")
match notify_user(mailer, "42")
Ok(_) -> stdio.println("sent")
Err(e) -> stdio.eprintln("error: ${e}")
Read the type of notify_user. It declares SendEmail, not Net. Someone reading the signature learns that this function sends email; they do not learn that the underlying transport is HTTP-over-something. The implementation choice is invisible at the call site, which is the point: SendEmail is the contract.
The manifest reflects this. notify_user appears with declared_capabilities: ["SendEmail"], no Net in sight. An auditor reading the SBOM sees the high-level authority surface, not the implementation transport. A reviewer who wants to know what SendEmail ultimately translates to looks at the user_defined_capabilities section of the manifest, which lists every type that implements it.
Why this matters for libraries
Capability-typed libraries are the unique contribution of Capa. In a normal language, a library's authority surface is documented in the README and enforced by nothing. In Capa it is a type, the compiler enforces it, the SBOM emits it.
Library authors get a stable contract: "to use this library you give it a SendEmail; to give it one you decide what implementor to build." Callers get reasoning power: "anything that does not take a SendEmail cannot send mail." Auditors get an SBOM where declared_capabilities is meaningful at the level the user actually cares about.
The pattern scales. A database library exposes capability QueryDB; a message-queue client exposes capability PublishMessage; a JWT signer exposes capability SignToken. None of them appear as built-ins because Capa cannot anticipate every domain; the language makes it cheap for libraries to add their own.
The two error modes
First: a struct that holds a built-in cap but does not implement a user-defined cap.
type Holder {
net: Net
}
error: type 'Holder' has a capability field 'net' but does not
implement any user-defined capability; capabilities cannot
appear as struct fields outside the cap-bearing pattern
1 | type Holder {
^
Either remove the cap field or pair the struct with an impl SomeCap for Holder. The cap-bearing relaxation is opt-in: you have to declare what capability the struct embodies before you are allowed to wrap a built-in inside it.
Second: a method whose signature does not match the trait.
impl SendEmail for SmtpMailer
fun send(self, to: String) -> Result<Unit, IoError>
return Ok(())
error: impl SendEmail for SmtpMailer: method 'send' has the wrong
signature; expected (self, String, String, String) ->
Result<Unit, IoError>, got (self, String) -> Result<Unit, IoError>
The implementor's method signature must match the capability declaration exactly: same arity, same parameter types, same return type. Extra helper methods on the struct are fine; the contract is just that the declared method is there with the right shape.
Try this. Define capability StoreUser with a single method save(self, id: String, payload: String) -> Result<Unit, IoError>. Implement it with a struct FileStore that holds an Fs and writes one file per user. In main, narrow the Fs to "/var/users/" before constructing the FileStore.
Where you are now
You can define your own capability types and implement them in terms of the built-ins. Authority surfaces in your programs can speak the language of your domain (StoreUser, SendEmail) instead of the language of the runtime (Fs, Net).
The next chapter moves out of single-file programs: how to split your code into modules, control which names are visible, and find dependencies. Then chapter 12 puts it all together in a real CLI tool.