caps
Source: crates/lvcs-doc/src/kinds/caps/mod.rs.
/// The root of every grant chain (design §11): the owner's key and the
/// project name, whose [`ProjectRoot::project_id`] is the project id,
/// `HASH` of the canonical CBOR array (owner key bytes, name text), so
/// `lvcs://<project-id>` names a project without a registry. A daemon
/// accepts a `.lvcs/caps` owner only when its root's project id equals the
/// project id the daemon runs (the one `lvcs init` made, or the one
/// `lvcs join` was given): `lvcs join` learns the root from the caps
/// document's seed and checks it before it verifies any other change, so
/// the first peer it syncs from can't substitute an owner of its own.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProjectRoot {
pub owner: ParticipantKey,
pub name: String,
}
/// A grant's id: the `HASH` of its [`Grant`]'s canonical CBOR encoding
/// ([`Grant::id`]), so a renewal (a new window) is a new grant with a new
/// id and a fresh quota (design §15). `ChangeSig::grant` names it. Written
/// in `ID_ENCODING`. The all-zero id ([`GrantId::OWNER`]) names no grant:
/// the owner's own changes, which need none (chosen at the M3 freeze).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct GrantId(#[serde(with = "crate::cbor::fixed")] pub [u8; 32]);
/// One thing a grant lets its grantee do (design §11, §15). Chosen at the
/// M3 freeze: changes and gossip are separate capabilities, so a grant can
/// let a participant talk without writing.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Capability {
/// Sign changes to documents under the grant's `paths`: edits, mirror
/// saves, merge-back bundle changes, and control-document writes.
Write,
/// Publish gossip messages (notifications, responses, thread messages,
/// presence, typing) about documents under the grant's `paths`.
Message,
/// Attest the named channel, an action's (`tests`, `staging`): a
/// runner's capability (design §7, §10, decision 297).
Attest(String),
/// Approve protected-path changes (design §15).
Approve,
/// Issue grants no wider than this one (design §11).
Delegate,
/// Read documents under the grant's `paths` over the daemon's API
/// (decision 300). It authorizes no change; a participant holding no
/// live `Read` grant reads everything, as before, and one holding any
/// reads only what they cover. A `Read` grant to [`ANYONE`] lets an
/// unauthenticated client read those paths.
Read,
}
/// The grantee key of a grant to anyone (decision 300): the all-zero key,
/// which is no Ed25519 public key, so nothing ever signs as it. Only a
/// `Read` grant means anything for it.
pub const ANYONE: ParticipantKey = ParticipantKey([0; 32]);
/// A grant's terms (design §15, decision 146), every one optional and
/// left out of the grant's encoding when absent. `harness` and `model`
/// name what the grantee runs, for attribution only: a receiving daemon
/// shows them beside the sender's `participant` tier, computed from the
/// grant `authorize` found, never from a message (decision 147), and no
/// check reads them but their length: each is at most
/// `GRANT_TERM_MAX_BYTES` (decision 178), and a caps change putting a
/// grant with a longer one is `out_of_scope`. `quota` is decision 70's change quota, enforced only
/// when set: in each document, a change signed under the grant is invalid
/// on every peer when it comes after `quota` among its actor's changes
/// under the grant, in Automerge `seq` order, and such a change's actor id
/// is the grant's own (`AuthorId::granted`, decision 125); the daemon signs
/// under a grant without a quota before a quota'd one, and under the
/// largest quota first (decision 178). `sign_per_sec`, `sign_burst`, and
/// `gossip_per_sec` are this grant's values for the grantee's signing and
/// gossip buckets, an absent one giving the `[limits]` default in force;
/// the grantee is metered at the largest over its live grants (decision
/// 178), so no grant lowers the rate another gives. An issuer may set any
/// value, since limits bound load, not authority. A renewal copies the
/// terms. `alias` is the name the project owner gives the grantee
/// (decisions 243 and 247), for display and addressing only, no
/// authority: at most `ALIAS_MAX_BYTES` of lowercase ASCII letters,
/// digits, and `-`, starting with a letter ([`alias_valid`]), on a grant
/// the owner issued, else the grant is `out_of_scope` on every peer. An
/// alias is bound for the life of the project to the grantee of the first
/// grant naming it, ordered by `not_before_s` then grant id, whether that
/// grant is live, expired, or revoked; a later grant naming it for another
/// grantee gives that grantee no alias.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct GrantTerms {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub harness: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quota: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sign_per_sec: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sign_burst: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gossip_per_sec: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
/// The grant is a device certificate (decision 300): its issuer
/// delegates to the grantee, a device key paired with `lvcs auth
/// login`, which `lvcs auth status` lists and `lvcs auth logout`
/// revokes. No rule reads it.
#[serde(default, skip_serializing_if = "core::ops::Not::not")]
pub device: bool,
}
/// A capability grant (design §11, §15, decision 58): `issuer` (the owner,
/// or a holder of [`Capability::Delegate`]) lets `grantee`, on the
/// [`GrantTerms`] `terms`, do `capabilities` on `paths` (gitignore-syntax
/// patterns relative to the project root, like every LVCS path pattern,
/// matched with the `ignore` crate's gitignore matcher, which `lvcs-doc`
/// links from the M3 freeze; sorted; `**` for everything)
/// within the window `not_before_s..not_after_s` in Unix seconds. A change
/// is within the window when the window contains the change's own Automerge
/// time (design §3), which its hash covers, so every peer decides alike
/// whatever its clock says; `not_after_s` is exclusive and absent only for
/// a grant with no expiry. `capabilities` is sorted.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Grant {
pub issuer: ParticipantKey,
pub grantee: ParticipantKey,
pub terms: GrantTerms,
pub paths: Vec<String>,
pub capabilities: Vec<Capability>,
pub not_before_s: i64,
pub not_after_s: Option<i64>,
}
/// A grant with its issuer's `SIGNATURE` over [`Grant::signing_input`].
/// The `.lvcs/caps` document holds every signed grant; a grant whose
/// signature or chain fails is ignored by every peer.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignedGrant {
pub grant: Grant,
#[serde(with = "crate::cbor::fixed")]
pub sig: [u8; 64],
}
/// The static signed capability file `lvcs init` writes and that seeds the
/// `.lvcs/caps` document (design §11), at `.lvcs/capabilities.cbor`: its
/// format version `v` (1), the project id's bytes, the project name and
/// the owner's key (the [`ProjectRoot`], whose project id must equal
/// `project`, so a peer that knows only the id can check the owner), the
/// seed grants (`lvcs init` writes none), and the owner's `SIGNATURE` over
/// [`CapsFile::signing_input`]. Encoded as canonical CBOR. The caps
/// document's seed change carries `name`, `owner`, and the grants, so a
/// peer that syncs `.lvcs/caps` learns the root.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapsFile {
pub v: u64,
#[serde(with = "crate::cbor::fixed")]
pub project: [u8; 32],
pub name: String,
pub owner: ParticipantKey,
pub grants: Vec<SignedGrant>,
#[serde(with = "crate::cbor::fixed")]
pub sig: [u8; 64],
}