Skip to main content

godspeed/
cap.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Capabilities: the rights a capability carries, and the operations on one.
3//!
4//! # What a capability is here
5//!
6//! An unforgeable token naming a resource, the rights you hold over it, and the generation it was
7//! minted at (CLAUDE.md 7.2). Holding one is necessary and sufficient authority for what it permits.
8//! There is no other way to act: no ambient authority, no privilege inherited from who you are.
9//!
10//! # What this module is, and what it replaced
11//!
12//! `gs::cap` used to be the FILE module - 256 lines whose doc opened "a file as a capability" and
13//! which held [`File`](crate::file::File) and nothing else. So a program needing to acquire, derive
14//! or drop an ordinary capability found nothing here and reached into `godspeed_sdk`, which is the
15//! layer a program is not supposed to need. The file half now lives in [`crate::file`].
16//!
17//! The rights constants stayed, because they were never file-specific: [`READ`] and [`WRITE`] are
18//! capability rights (CLAUDE.md 7.4), and `cap::READ` at an `fs.open` call site reads correctly.
19//!
20//! # The three properties worth knowing before you use one
21//!
22//! **Rights only narrow.** A copy never carries more than its source, and there is no path back up.
23//! [`duplicate`] makes an identical copy to hand away; narrowing happens where a capability is
24//! minted or granted (CLAUDE.md 7.3), not on the copy.
25//!
26//! **A capability can go stale.** Every one carries a generation, and the resource bumps its own when
27//! it dies or is replaced. The next use of a stale capability fails with
28//! [`Error::Unreachable`](crate::Error) or [`Error::Revoked`](crate::Error) rather than reaching the
29//! new instance - `gs::Error` has no `EndpointDead` variant, so there is nothing else to match. That
30//! is not a fault to route around: it is the system telling you the thing you held is gone, and
31//! [`reacquire`] is how you answer it.
32//!
33//! **Transfer MOVES.** Sending a capability with [`send_granting`](crate::ipc) removes it from your
34//! table. If the send fails it stayed, and it is yours to reclaim; if it succeeded it is not yours
35//! any more. Either way the outcome of the send decides what you may do next, so it is never
36//! ignorable (CLAUDE.md 8.5).
37
38use godspeed_sdk::capability::CapHandle;
39use godspeed_sdk::service_context::ServiceContext;
40
41use crate::error::Error;
42
43/// Read from the resource (CLAUDE.md 7.4) - a file's contents, a socket's datagrams.
44pub const READ: u8 = 1 << 0;
45
46/// Write to the resource (CLAUDE.md 7.4) - a file's contents, a socket's datagrams.
47pub const WRITE: u8 = 1 << 1;
48
49/// Ask `fs` for an append-only capability: writes through it may only move FORWARD.
50///
51/// **A flag in `fs`'s OPEN request, not a kernel right** - the kernel's rights are bits 0-5, and
52/// this bit never reaches it: `fs` mints an ordinary WRITE capability and records the restriction
53/// against the resource. Do not pass it where a kernel right is expected.
54///
55/// Enforced by `fs` against a HIGH-WATER MARK - the furthest offset written through this
56/// capability - not against the file's size: a write below the mark is refused
57/// ([`Error::PermissionDenied`](crate::Error)). The first write may land anywhere, so this does not
58/// protect content that existed before the open, and a fresh open starts a fresh mark.
59pub const APPEND: u8 = 1 << 6;
60
61/// Transfer this capability onward (CLAUDE.md 7.4).
62///
63/// A capability handed out WITHOUT this right cannot be passed on by its holder. Grant it only when
64/// the receiver is meant to re-delegate: rights narrow on transfer and never widen, so the floor you
65/// set here is the floor for every copy that follows.
66pub const GRANT: u8 = 1 << 4;
67
68/// A capability this service holds, by the slot the kernel put it in.
69///
70/// Thin on purpose: the value is an index into this task's capability table, and the kernel is what
71/// gives it meaning. Wrapping it in a type stops it being confused with the resource ids, endpoint
72/// ids and file descriptors it is NOT.
73#[derive(Clone, Copy, PartialEq, Eq, Debug)]
74pub struct Cap(pub(crate) CapHandle);
75
76impl Cap {
77    /// The raw slot, for the rare call that still needs the SDK.
78    ///
79    /// Present so that reaching the lower layer is a visible, deliberate step rather than a reason to
80    /// abandon this module. If you find yourself calling this often, the missing thing belongs here.
81    pub fn handle(self) -> CapHandle {
82        self.0
83    }
84
85    /// Wrap a handle the SDK produced.
86    pub(crate) fn from_handle(h: CapHandle) -> Self {
87        Cap(h)
88    }
89}
90
91/// Adopt a handle an SDK call outside this library returned - a spawn that hands back an endpoint, a
92/// minted resource, a capability looked up by contract name - so the rest of its life is spent here.
93///
94/// This grants nothing and forges nothing. A handle is a slot number anyone can type; what makes a
95/// capability unforgeable is the kernel, which checks the slot, its generation and its rights on every
96/// use (CLAUDE.md 7.3). Wrapping a number that is not a capability yields a `Cap` every call refuses.
97impl From<CapHandle> for Cap {
98    fn from(h: CapHandle) -> Self {
99        Cap(h)
100    }
101}
102
103/// Acquire a SEND capability to a service, by name, from the kernel's name directory.
104///
105/// This is how a client finds a service it was not wired to at spawn, and how it finds the NEW
106/// instance after one died (CLAUDE.md 14.2). It is gated: the kernel answers only for a name that
107/// was among the send peers in this task's spawn request, or for a task holding the broad
108/// `ACQUIRE_ANY` authority (the shell, the supervisor, test probes). That is what stops name
109/// resolution being an ambient back door around the spawn grant.
110///
111/// [`Error::NotFound`] covers every refusal alike: a name this task may not acquire, a name not
112/// (yet, or currently) registered - a peer mid-restart is the common case - and a full capability
113/// table. The SDK call returns no reason to tell them apart.
114pub fn acquire(ctx: &ServiceContext, name: &str) -> Result<Cap, Error> {
115    match ctx.acquire_send_cap(name) {
116        Some(h) => Ok(Cap(h)),
117        None => Err(Error::NotFound),
118    }
119}
120
121/// Acquire a SEND capability that may itself be passed on ([`GRANT`]).
122///
123/// Separate from [`acquire`] rather than a flag on it, because handing out a re-delegatable
124/// capability is a different decision from using one and should read differently at the call site.
125pub fn acquire_grantable(ctx: &ServiceContext, name: &str) -> Result<Cap, Error> {
126    match ctx.acquire_send_grant_cap(name) {
127        Some(h) => Ok(Cap(h)),
128        None => Err(Error::NotFound),
129    }
130}
131
132/// Re-acquire a service by name after its previous instance died.
133///
134/// The answer to an [`Error::Unreachable`](crate::Error): the name is stable, the instance is not
135/// (CLAUDE.md invariant 11). Returns `true` if a fresh capability was installed.
136///
137/// **Re-acquiring the endpoint is necessary and not sufficient.** Anything you derived from the DEAD
138/// instance - an open file, a socket, a connection id, a cached copy of its state - was issued by a
139/// service that no longer exists and the new one has never heard of. Re-establish those too, or you
140/// are holding a handle the new instance will refuse (CLAUDE.md 14.3).
141pub fn reacquire(ctx: &ServiceContext, name: &str) -> bool {
142    ctx.reacquire_by_name(name)
143}
144
145/// Duplicate a capability you hold, to hand the copy away while keeping the original.
146///
147/// The copy carries the SAME resource, generation and rights - this does not narrow, and there is no
148/// rights argument because the kernel's `DeriveCap` does not take one. Narrowing is real (CLAUDE.md
149/// 7.3) and happens where a capability is minted or granted, not here.
150///
151/// Requires the source to hold [`GRANT`]: a capability you may not pass on is also one you may not
152/// copy for passing on. Fails if it lacks GRANT, has gone stale, or the table is full - and those
153/// three are not distinguished, which is a limitation of the syscall rather than of this wrapper.
154pub fn duplicate(ctx: &ServiceContext, cap: Cap) -> Result<Cap, Error> {
155    match ctx.derive_cap(cap.0) {
156        Some(h) => Ok(Cap(h)),
157        None => Err(Error::PermissionDenied),
158    }
159}
160
161/// Drop a capability from this service's table.
162///
163/// Authority you no longer need is authority you should not hold. This is also how a table slot is
164/// returned after a transfer failed and left the capability with you (CLAUDE.md 8.5).
165pub fn remove(ctx: &ServiceContext, cap: Cap) {
166    ctx.remove_cap(cap.0);
167}
168
169/// A grantable capability to THIS service's own endpoint, to hand to someone who must reply.
170///
171/// The shape behind request/reply: the caller sends one of these so the replier has somewhere to
172/// answer, and the kernel wakes the caller with `ReplyDead` if the replier dies holding it, so a
173/// reply that will never come is reported rather than waited for (CLAUDE.md 8.6).
174pub fn self_grant(ctx: &ServiceContext) -> Result<Cap, Error> {
175    match ctx.self_grant_handle() {
176        Some(h) => Ok(Cap(h)),
177        None => Err(Error::PermissionDenied),
178    }
179}