Credentials and enrollment
Where a node's credential comes from, how to keep it, what scopes it carries, and how it is revoked.
A node connects with a bearer string and nothing else. Where that string comes from — a secret manager, a file, a person approving a code — is the program's business. The library holds no credential of its own and picks no path to store one at.
Enrollment happens over HTTPS before the websocket exists, so none of it is on the Zyris wire. The format of the credential and the enrollment flow are Attacca's choices; this page describes them as Attacca implements them.
The credential
On Attacca a credential is a long-lived zc_ bearer, issued to one program on one system. It
is hashed at rest, carries the scopes chosen when it was issued, and never expires. It is
revoked from Attacca's web UI.
In Rust it is a serde Credential:
| Field | Meaning |
|---|---|
secret | The zc_… string. What Node::connect presents. Also available as credential.secret(). |
system | The machine, as the approving person picked or created it (id, name, slug). |
program | The program it was issued to. Its id is the credential's own. |
scopes | What every node made with this credential may do. |
owner_email | The account it was issued under — worth printing once, to catch the wrong deployment. |
Credential's Debug output redacts the secret, so an accidental log line does not leak it.
Two ways to get one
Device grant — a person approves a code
For a program that starts unconfigured on a machine with someone nearby. It follows RFC 8628.
use zyris::enroll::{EnrollRequest, Progress};
let mut enrollment = zyris::enroll(
server_url,
EnrollRequest {
program: "my-node".into(), // fixed on the credential
system_hint: zyris::machine_name().unwrap_or_default(), // preselects this machine
platform: std::env::consts::OS.into(),
scopes: vec!["sessions:write".into(), "agents:read".into()],
},
)
.await?;
let code = enrollment.code();
println!("open {} and enter {}", code.verification_uri, code.user_code);
let credential = loop {
match enrollment.poll().await? {
Progress::Waiting { .. } => {} // poll() paces itself
Progress::Granted(credential) => break credential,
Progress::Lapsed => {
enrollment.renew().await?;
let code = enrollment.code();
println!("open {} and enter {}", code.verification_uri, code.user_code);
}
Progress::Denied => return Err("declined".into()),
}
};- The program asks for a code. It shows the user an 8-character code and a URL.
- The user opens the URL on any device with a browser, enters the code, picks the system (the one
named by
system_hintby default, or a new one), reviews the program name and scopes, and approves. - The next
poll()returns theCredential.
poll() honours the server's interval and slow_down itself, so the loop needs no sleep of its
own. The library prints nothing: showing the code — a printed line, a panel in a TUI, a line in a
container log — is the program's decision. Enrollment needs the enroll feature.
The device endpoints are derived from the websocket URL by cutting it at /zyris/, so a program
cannot enroll against one deployment while connecting to another.
Web — issued in Attacca, pasted in
For a machine with nobody at it — a container, a CI runner, an image-baked node. In Attacca's web
UI, pick a system, a program name and scopes, and the zc_ is shown once. It is the same kind
of credential as a device grant produces. Hand it to the program through a secret:
let secret = std::env::var("ZYRIS_CREDENTIAL")?;
let link = node.connect(zyris::DEFAULT_SERVER_URL, &secret).await?;A program provisioned this way never touches enrollment and does not need the enroll feature.
Storing it
A credential never expires and never rotates, so write it once and read it back on every start.
// Save
std::fs::write(&path, serde_json::to_vec(&credential)?)?;
// Load
let credential: zyris::Credential = serde_json::from_slice(&std::fs::read(&path)?)?;Put it wherever secrets belong on that platform — a file readable only by its owner, the OS keychain, a Kubernetes Secret. Treat it like a password: anyone holding it can connect as that program.
Scopes
Scopes belong to the credential, are chosen when it is issued, and apply to every node made with it. The approving person may grant fewer than were asked for.
A node that only serves tools needs no scopes at all. Creating nodes needs none either — that is
what a credential is for. Scopes gate what a node may do when it calls back into the deployment,
which on Attacca means attacca_api:
| Scope | Allows |
|---|---|
agents:read / agents:write | Listing and creating agents |
projects:read / projects:write | Projects |
sessions:read / sessions:write | Reading, creating and messaging sessions |
jobs:read / jobs:write | Scheduled jobs |
works:read / works:write | Works — multi-task runs with their own branches |
files:read / files:write | Files |
artifacts:read / artifacts:write | Artifacts |
kanban:read / kanban:write | Kanban boards |
events:read | Account events and turn event streams |
peers:write | Publishing and looking up peer addresses for direct transfers |
Asking for a scope the deployment does not know fails the whole request with
EnrollError::ScopeUnknown, so ask only for what you use.
Revocation
A credential is revoked from Attacca's web UI, directly or by deleting its system. Its live nodes
are closed with websocket code 4401, and from then on the upgrade is refused with HTTP 401:
connect returns ConnectError::Unauthorized.
For an already-established link, Link::wait_closed() returns ConnectError::Revoked and the
reconnect loop stops. A later connection attempt returns ConnectError::Unauthorized; a retired
token format (such as the old znt_ node tokens) is refused the same way.
Handle both as "discard the stored credential and require fresh authorization". Do not keep retrying a revoked bearer or silently enroll without showing the new approval flow.
That is the only case that reaches a client automatically. A credential the client drops itself — a logout, a lost file — stays valid until someone revokes it by hand, because nothing tells Attacca it was dropped. Approving again just issues a second credential with the same program name.