ZyrisDocs

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 enroll

With 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 stop

Ask 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
Cargo.toml
[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.

src/main.rs
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

src/main.rs
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 run

Next 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.

On this page