Expand description
§Ag^id
The same input always produces the same identifier. On every platform. Stable across the v1.x line.
For input (DeriveDomain::User, b"alice@example.com"):
did:agid:2mDwJhrvWdJsqHAhRTQWpaLgWmnTZxEZJv6hnDmjiYtt ← Linux x86-64
did:agid:2mDwJhrvWdJsqHAhRTQWpaLgWmnTZxEZJv6hnDmjiYtt ← macOS ARM
did:agid:2mDwJhrvWdJsqHAhRTQWpaLgWmnTZxEZJv6hnDmjiYtt ← Raspberry Pi
did:agid:2mDwJhrvWdJsqHAhRTQWpaLgWmnTZxEZJv6hnDmjiYtt ← WASM in browserNo database. No coordination. No randomness.
§Why
UUID v4 is random. That’s fine until two nodes need to agree on the same identifier for the same entity — without talking to each other. Ag^id solves this with a single BLAKE3 hash: same input → same ID, always.
This matters for:
- Distributed systems — deterministic entity IDs across nodes
- Content addressing — stable document IDs that survive moves/renames
- Replayable AI pipelines — entity references that survive serialisation
- Edge/offline apps — generate IDs without a server
§Usage
[dependencies]
ag_id = "0.1"use ag_id::{DeriveDomain, Did};
// Derive an identifier
let id = Did::derive(DeriveDomain::User, b"alice@example.com");
println!("{}", id); // did:agid:2mDwJhrvWdJsqHAhRTQWpaLgWmnTZxEZJv6hnDmjiYtt
// Same input, same result — every time, everywhere
let id2 = Did::derive(DeriveDomain::User, b"alice@example.com");
assert_eq!(id, id2);
// Different domains → different IDs (even with same input)
let doc_id = Did::derive(DeriveDomain::Document, b"alice@example.com");
assert_ne!(id, doc_id);
// Round-trip through the wire form
let s = id.to_did_string();
let parsed: Did = s.parse().expect("did:agid:<base58>");
assert!(parsed.eq_bytes(&id));serde integration is opt-in via the serde feature flag. With it enabled,
Did serialises as a "did:agid:<base58>" string in JSON/YAML/TOML and as 32
raw bytes in binary formats (bincode, postcard, MessagePack).
For a runnable end-to-end walkthrough see examples/basic.rs
and examples/parsing.rs.
§Domains
| Domain | Byte | Use case |
|---|---|---|
User | 0x01 | Human or machine identity |
Document | 0x02 | File, document, content |
Session | 0x03 | Session or transaction |
Device | 0x04 | Physical or virtual device |
Concept | 0x05 | Semantic concept |
Custom(u8) | any | Your namespace |
Domain separation is a security property: a User ID can never equal a Document ID
even if both were derived from the same bytes.
§How it works
BLAKE3("agid:v1:" || domain_byte || input) → 32 bytes → did:agid:<base58>That’s it. No state. No clock. No random. Pure function.
§Features
no_stdcompatible (default:stdfeature enabled)- Zero heap allocations in the hot path
- BLAKE3-256 hash;
Hasher::new()(unkeyed) mode only - Wire form is a W3C DID URI ABNF–conformant string (
did:agid:<base58>); a W3C DID method registration, resolver, and DID Document layer are tracked on the roadmap but are not implemented in this crate today (seeDESIGN.md§6) - Cross-platform by construction (verified by independent Python witness in
conformance/)
§Performance
Indicative numbers from cargo bench --bench throughput on a Ryzen 9 7900X
(BLAKE3 with AVX-512). Reproduce on your hardware before quoting — see
BENCHMARKS.md for full methodology, environment, and a
larger result table.
derive/16 time: [67.2 ns] throughput: [227.0 MiB/s]
derive/1024 time: [833.4 ns] throughput: [1.14 GiB/s]
derive/65536 time: [9.96 µs] throughput: [6.13 GiB/s]§What this crate is and is not
This crate is:
- A deterministic identifier primitive: pure function
(domain, input) → 32 bytes. - A canonical wire form:
did:agid:<base58>, conforming to the W3C DID URI ABNF. - A domain-separated derivation scheme: same input in different
DeriveDomains produces different identifiers, enforced by the protocol prefix and the 1-byte domain in the hash input. - A portable, protocol-style spec with cross-language test vectors in
test-vectors/v1.jsonand an independent Python witness inconformance/.
This crate is not (yet):
- A registered W3C DID method. The
agidname has not been submitted to the W3C DID Method registry; submission is on the roadmap. - A DID resolver. There is no function returning a structured
DidDocumentfrom adid:agid:URI in this crate today. - A full DID Document layer. No JSON-LD context, no
servicearrays, noverificationMethodarrays. - An authentication, credentials, signing, or attestation system.
Didis a name, not a credential — it proves nothing about who computed it. Do not use==betweenDidvalues as a proof of control or possession. - An encryption scheme. Ag^id does not encrypt anything. The only security property is collision resistance from BLAKE3-256 and domain separation from the 8-byte prefix plus 1-byte domain.
§Limitations
Read these before using Ag^id in security-sensitive contexts.
- Not constant-time.
Did::eqis a byte-by-byte compare. Do not use it to compare secret authenticators or capability tokens — use a constant-time comparator (e.g.subtle::ConstantTimeEq) instead. - Collision strength is 256-bit. Cryptographically strong against random collisions, but identifier length (≤44 base58 chars) is the user-visible surface — not security strength.
- No deletion, no rotation. A
Didis a pure function of(domain, input). Ifinputis sensitive (e.g. an email address), the resultingDidis a stable pseudonym for that input forever. There is no server-side invalidation. Hash inputs you control, not raw PII, when this matters. - Custom domain separation is enforced by discipline.
DeriveDomain::custom(b)lets callers pick any non-zero byte; if two systems pick the same byte for different semantics, their IDs collide by design. Reserve0x01..=0x05for the built-in variants and document your custom-byte allocations. - Not for adversarial uniqueness. A determined attacker can grind inputs
until two map to the same prefix substring. The full 32-byte ID resists
this, but truncated displays do not. Never truncate a
Didfor uniqueness checks. - Stability contract is v1.x only. The protocol prefix
(
b"agid:v1:") and theDomainbyte assignments are part of the semver-major contract. Av2would intentionally produce different IDs.
§Licence
Licensed under either of:
- MIT licence (
LICENSE-MIT) - Apache Licence, Version 2.0 (
LICENSE-APACHE)
at your option.
© 2026 Mikhail Kostan / AuriGlyph.
§Further reading
SPEC.md— formal protocol specification (re-implementation reference).DESIGN.md— design rationale and prior-art comparison.ROADMAP.md— pre-1.0 stabilisation, post-1.0 plans, v2 outlook.SECURITY.md— threat model and disclosure policy.BENCHMARKS.md— methodology + environment block.CONTRIBUTING.md— how to send patches.test-vectors/v1.json— canonical cross-language vectors (positive + negative cases).conformance/README.md— conformance suite overview and how to add a new-language witness.
Built by AuriGlyph. Project home: https://auriglyph.com/projects/ag_id.
§Ag^id (crate ag_id)
The same input always produces the same identifier. On every platform. Forever.
use ag_id::{DeriveDomain, Did};
let id = Did::derive(DeriveDomain::User, b"alice@example.com");
let id2 = Did::derive(DeriveDomain::User, b"alice@example.com");
assert_eq!(id, id2); // always
assert!(id.to_did_string().starts_with("did:agid:"));Re-exports§
pub use error::Error;
Modules§
- error
- Error types for deterministic identifier operations.
Structs§
- Did
- A deterministic identifier.
Enums§
- Derive
Domain - Semantic domain accepted by derivation APIs.
- Domain
- Semantic domain — prevents cross-domain ID collisions.
Constants§
- DID_
PREFIX - Canonical DID URI prefix for this method.
Functions§
- derive
- Derive a deterministic identifier from a derivation domain and input bytes.
- derive_
str - Derive from a string slice.