Quickstart
Build a node with one tool, connect it to Attacca, and have an agent call it.
By the end of this page you will have a Rust program that announces a hello capability with one
tool, hello.greet, and an agent on your Attacca account that can call it.
You need a recent stable Rust toolchain, a C compiler (the TLS provider builds C code), and an Attacca account.
1. Try the example
The repository ships the whole library end to end in one runnable file. Before writing your own, run it once:
git clone https://github.com/attacca-cc/zyris-protocol
cd zyris-protocol
cargo run -p zyris --example hello --features enrollWith nothing configured it enrolls against attacca.cc and prints a URL and an 8-character
code. Open the URL on any device with a browser, enter the code and approve. The example prints the
credential it was issued, then:
serving hello.greet as laptop/hello/hello; Ctrl-C to stopAsk an agent on the same account to call hello.greet. The node answers Hello World.
Keep the credential
The example prints the zc_ credential once because it has nowhere to store it. It never
expires. Set ZYRIS_CREDENTIAL to it to skip enrolling next time, and revoke it in Attacca when
you are done.
2. Create your own node
cargo new my-node
cd my-node[dependencies]
zyris = { git = "https://github.com/attacca-cc/zyris-protocol", features = ["enroll"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"] }
serde = { version = "1", features = ["derive"] }
schemars = "1"enroll adds the 8-character-code flow and turns on the tls-ring TLS provider. A node that is
handed its credential from a secret manager does not need enroll, but it still needs a TLS
provider to reach wss:// — see Connecting.
3. Declare a capability
A capability is a trait. The macro turns it into a descriptor, a server wrapper and a client.
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// What `hello.greet` hands back. Field doc comments become the JSON Schema a model reads.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Greeting {
/// The greeting itself.
pub message: String,
}
#[zyris::capability(name = "hello", version = 1)]
pub trait Hello {
/// Say hello, optionally to someone in particular.
async fn greet(&self, name: Option<String>) -> zyris::Result<Greeting>;
}
struct HelloWorld;
#[zyris::async_trait]
impl Hello for HelloWorld {
async fn greet(&self, name: Option<String>) -> zyris::Result<Greeting> {
let message = match name {
Some(who) => format!("Hello, {who}!"),
None => "Hello World".to_string(),
};
Ok(Greeting { message })
}
}Each method's doc comment becomes that tool's description. Write it for the model that will read it.
4. Get a credential and connect
use zyris::enroll::{EnrollRequest, Progress};
use zyris::{Node, NodeKind};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let secret = match std::env::var("ZYRIS_CREDENTIAL") {
Ok(secret) => secret,
Err(_) => enroll().await?,
};
let link = Node::builder()
.name("my-node")
.kind(NodeKind::Service)
.capability(HelloServer(HelloWorld))
.build()?
.connect(zyris::DEFAULT_SERVER_URL, &secret)
.await?;
println!("serving as {:?}", link.address().map(|a| a.path()));
tokio::select! {
closed = link.wait_closed() => closed?,
_ = tokio::signal::ctrl_c() => link.disconnect().await,
}
Ok(())
}
async fn enroll() -> Result<String, Box<dyn std::error::Error>> {
let mut enrollment = zyris::enroll(
zyris::DEFAULT_SERVER_URL,
EnrollRequest {
program: "my-node".into(),
system_hint: zyris::machine_name().unwrap_or_default(),
platform: std::env::consts::OS.into(),
scopes: vec![], // a node that only serves tools needs no scopes
},
)
.await?;
let code = enrollment.code();
println!("open {} and enter {}", code.verification_uri, code.user_code);
loop {
match enrollment.poll().await? {
Progress::Waiting { .. } => {}
Progress::Granted(credential) => {
println!("store this, it never expires: {}", credential.secret());
return Ok(credential.secret);
}
Progress::Lapsed => {
enrollment.renew().await?;
let code = enrollment.code();
println!("open {} and enter {}", code.verification_uri, code.user_code);
}
Progress::Denied => return Err("the request was declined".into()),
}
}
}HelloServer was generated by #[zyris::capability]. connect returns a Link that keeps the
connection up across drops, so the program only has to decide when to stop.
cargo runNext steps
-
Capability addons — reuse files, terminals, desktop control and file transfer beside your own capability.
-
Concepts — what a system, a credential and a node are, and how they are named.
-
Writing capabilities — errors, streaming tools, call limits and versioning.
-
Credentials and enrollment — storing a credential, scopes and revocation.
-
zyris-hello — a larger reference node that stores its credential on disk, transfers files and drives a display.