Creating and Using Capabilities
Previous examples have shown how to acquire capabilities in a manifest file. Here, we'll show how a package that does not come "built in" to Kinode can create its own capabilities and how other processes can use them.
Recall that capabilities are tokens of authority that processes can use to authorize behaviors and are managed by the kernel. In userspace, there are two common patterns for using capabilities: requesting/granting them in the package manifest, or attaching them to a message. The first pattern is more common, and generally matches the intuition of the program's end-user: when they install an app, they are presented with a list of actions that the app will be able to perform, such as messaging the "eth" process to read blockchain data or access the files of a specific other package.
There is no need to register capabilities that can be granted. When another process requests them, the package manager / app store, which has kernel-messaging authority, can spawn them if a user approves. This allows capabilities to be granted before they are needed, even if the relevant package is not installed yet.
To require that a capability exist in order to fulfill a message, one can check for its existence in the capabilities
field of the message.
// each request requires one of read-name-only, read, add, or remove
if let Some(capabilities) = capabilities {
let required_capability = Capability::new(
&state.our,
serde_json::to_string(&match request {
contacts::Request::GetNames => contacts::Capability::ReadNameOnly,
contacts::Request::GetAllContacts | contacts::Request::GetContact(_) => {
contacts::Capability::Read
}
contacts::Request::AddContact(_) | contacts::Request::AddField(_) => {
contacts::Capability::Add
}
contacts::Request::RemoveContact(_) | contacts::Request::RemoveField(_) => {
contacts::Capability::Remove
}
})
.unwrap(),
);
if !capabilities.contains(&required_capability) {
return (
contacts::Response::Err("Missing capability".to_string()),
None,
);
}
}
This code is run on each incoming request to the contacts
process.
Depending on the kind of request, the code generates one of four different required capabilities and checks whether the necessary one is present in the capabilities
field of the message.
If not, the process responds with an error message.
This example uses a similar API as the contacts
app included in the default Kinode distribution: for a guide to use the actual contacts system primitive, see Managing Contacts.
Note that the format of the capability is presented in a WIT API file alongside the request and response types. This allows other processes to easily produce the correct capability when requesting it.
Now, take a look at the manifest for the contacts-test
process.
{
"process_name": "contacts-test",
"process_wasm_path": "/contacts_test.wasm",
"on_exit": "None",
"request_networking": false,
"request_capabilities": [
"contacts:capabilities-test:doria.kino",
{
"process": "contacts:capabilities-test:doria.kino",
"params": "ReadNameOnly"
},
{
"process": "contacts:capabilities-test:doria.kino",
"params": "Read"
},
{
"process": "contacts:capabilities-test:doria.kino",
"params": "Add"
},
{
"process": "contacts:capabilities-test:doria.kino",
"params": "Remove"
}
],
"grant_capabilities": [
"contacts:capabilities-test:doria.kino"
],
"public": false
}
This manifest requests all four capabilities from the contacts
process.
Naturally, the correct package name and publisher must be used here.
The "params"
field must match the JSON serialization of the capability type that lives in the WIT API:
enum capability {
read-name-only,
read,
add,
remove,
}
Let's see contacts-test
using these capabilities in action.
let contacts_process =
Address::from((our.node(), "contacts", "capabilities-test", "doria.kino"));
// All of these capabilities were requested in the manifest,
// so we can create them here and attach them to our requests.
// If they were not in the manifest or otherwise acquired,
// we could still create the objects, but they would not be
// attached to our requests and therefore the requests would fail.
let read_names_cap = Capability::new(
&contacts_process,
serde_json::to_string(&contacts::Capability::ReadNameOnly).unwrap(),
);
let read_cap = Capability::new(
&contacts_process,
serde_json::to_string(&contacts::Capability::Read).unwrap(),
);
let add_cap = Capability::new(
&contacts_process,
serde_json::to_string(&contacts::Capability::Add).unwrap(),
);
let remove_cap = Capability::new(
&contacts_process,
serde_json::to_string(&contacts::Capability::Remove).unwrap(),
);
kiprintln!("requesting all names from contacts");
let response = Request::to(&contacts_process)
.body(serde_json::to_vec(&contacts::Request::GetNames).unwrap())
.capabilities(vec![read_names_cap])
.send_and_await_response(5)
.unwrap()
.unwrap();
kiprintln!(
"response: {:?}",
serde_json::from_slice::<contacts::Response>(&response.body()).unwrap()
);
kiprintln!("requesting all names from contacts (without capability attached!)");
let response = Request::to(&contacts_process)
.body(serde_json::to_vec(&contacts::Request::GetNames).unwrap())
// no cap
.send_and_await_response(5)
.unwrap()
.unwrap();
kiprintln!(
"response: {:?}",
serde_json::from_slice::<contacts::Response>(&response.body()).unwrap()
);
kiprintln!("adding contact to contacts");
let response = Request::to(&contacts_process)
.body(
serde_json::to_vec(&contacts::Request::AddContact(
"mothu-et-doria.os".to_string(),
))
.unwrap(),
)
.capabilities(vec![add_cap])
.send_and_await_response(5)
.unwrap()
.unwrap();
kiprintln!(
"response: {:?}",
serde_json::from_slice::<contacts::Response>(&response.body()).unwrap()
);
kiprintln!("reading all contacts from contacts");
let response = Request::to(&contacts_process)
.body(serde_json::to_vec(&contacts::Request::GetAllContacts).unwrap())
.capabilities(vec![read_cap])
.send_and_await_response(5)
.unwrap()
.unwrap();
kiprintln!(
"response: {:?}",
serde_json::from_slice::<contacts::Response>(&response.body()).unwrap()
);
kiprintln!("removing contact from contacts");
let response = Request::to(&contacts_process)
.body(
serde_json::to_vec(&contacts::Request::RemoveContact(
"mothu-et-doria.os".to_string(),
))
.unwrap(),
)
.capabilities(vec![remove_cap])
.send_and_await_response(5)
.unwrap()
.unwrap();
kiprintln!(
"response: {:?}",
serde_json::from_slice::<contacts::Response>(&response.body()).unwrap()
);
When building each request (except for the one that specifically does not attach a capability, and fails as a result), a capability is attached in the request's builder pattern. Because the capabilities were requested in the manifest, they can be created here and used. If a capability did not exist in the manifest, and was not otherwise acquired during runtime, the capability would not show up for the message receiver, because the kernel validates each capability attached to a message and filters out invalid ones.
Go ahead and use kit to install this package, available here, and see how contacts-test
uses capabilities to interact with contacts
.