iam

IAM is an open, deterministic protocol for representing identity as a structured graph of entities, attributes, and trust relationships. It defines a minimal, implementation‑agnostic model for how identity data is expressed, linked, validated, and reasoned about across systems, without prescribing any runtime, product architecture, or operational workflow. IAM provides a stable semantic foundation that any platform, service, or application can adopt to ensure consistent interpretation of identity and trust across heterogeneous environments.


Why Identity Must Be Boring, Deterministic, and Offline

Modern identity systems confuse access with identity. Access is a runtime concern: login flows, federation, availability. Identity is not. Identity is a statement about existence and continuity. Any system that requires live connectivity or institutional availability to answer “who is this?” has already failed.

Boring because stable. The moment identity becomes dynamic or adaptive, it becomes policy or product. True identity does not negotiate; it records. Boring systems last: cryptographic hashes, append-only logs, DNS. Identity belongs to this class.

Deterministic because truth cannot depend on infrastructure. Same cryptographic material, same records, same result. Always. If identity changes depending on which resolver, network, or authority you ask, it is not a property of the subject but a side effect of the environment. Determinism is the only defense against silent mutation and reinterpretation.

Offline because identity must outlive systems. Networks fail. Organizations dissolve. Software rots. An identity that cannot be evaluated from archived data, without contacting any external service, is not an identity. It is a lease. History does not ask permission to be verified.

Most identity architectures solve the wrong problem: how to authenticate users today, federate trust between institutions, or monetize access. Valid problems, but not identity. Identity is the substrate beneath all of them. It must remain valid when none of them exist.


Specification

Below is the complete IAM protocol specification:

IAM

m=670625. iam-core 1.0.

Identity and trust graph. The constitution of JAM.

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in RFC 2119 and RFC 8174.


Overview

IAM defines how identities are created, how trust structures are formed, and how records are signed, ordered, and validated. It does not define messaging, memory, models, storage, transport, inactivity detection, succession, or network topology. Those belong to the JAM runtime layer that builds upon IAM.

IAM is small on purpose. It contains only explicit transitions over a stream of signed records. All derived states (inactivity, succession, failure recovery) are runtime interpretation.

The trust structure formed by ACCEPT records is a directed acyclic graph (DAG), not strictly a tree. A member MAY have multiple parents, which means multiple independent lineages to the root. This is both simpler and more honest: a person can be vouched for by more than one existing member.

If this specification is correct, everything above it can change without breaking the core.


Four words

name           username. lowercase ASCII slug. chosen once. never changes.
               part of key derivation.
incantation    pass phrase. secret. never stored. never transmitted.
nick           display label. freeform. mutable. cosmetic only.
seal           four-word visual fingerprint of the public key.

A user MUST remember two things: name and incantation. The seal is derived and shown for visual confirmation. The nick is decoration and plays no role in identity.


Canonical identity

The canonical identity of a member is its Ed25519 public key. The seal is a human-readable fingerprint, not an identifier. Implementations MUST use the public key for all identity comparisons and MUST NOT rely on the seal for uniqueness.

Seal collisions are possible by the birthday paradox at approximately 77000 identities in a single graph. When two seals collide, implementations MUST disambiguate by displaying a short public key suffix.


Name format

A name MUST match the regular expression ^[a-z0-9][a-z0-9._-]{0,31}$.

Lowercase ASCII letters, digits, period, underscore, and hyphen. First character MUST be a letter or digit. Maximum length is 32 characters.

Implementations MUST reject any other input before key derivation. No Unicode normalization, no case folding, no whitespace trimming. What the user typed is either a valid name or an error.

Names are not unique identifiers

Two different identities MAY share the same name slug. Such identities have different public keys (because their incantations differ), and IAM treats them as completely separate. The canonical identity is always the public key, never the name. When two members of the same graph share a name, runtime layers MUST disambiguate using public key suffixes (the same mechanism used for seal collisions).

A consequence of including name in the KDF salt is that two identities sharing a name slug also share the same Argon2id salt input. This slightly increases the efficiency of brute-force attacks against common names: an attacker who builds a precomputation for a popular name can reuse it against every identity sharing that slug. Users SHOULD avoid trivially common names (such as “alice”, “bob”, “admin”, “root”, “test”) for this reason. Less common names provide better isolation against shared-precomputation attacks without otherwise affecting identity uniqueness.


Incantation format

An incantation is a user-chosen pass phrase. It MUST match the regular expression ^[\x20-\x7E]{12,}$.

Printable ASCII only (0x20 through 0x7E), minimum 12 characters, no upper limit. This excludes all Unicode, all control characters, all zero-width characters, and all invisible whitespace variants that could silently differ between entry and recovery.

Implementations MUST NOT perform Unicode normalization, case folding, whitespace trimming, quote substitution, or any other transformation on the incantation bytes.

Implementations SHOULD provide a means for the user to see the exact bytes they are entering during creation, to avoid silent differences caused by autocorrect, input method editors, or keyboard layout quirks.

The minimum length of 12 filters out trivially short inputs. Real strength depends on unpredictability of the chosen phrase, not on length alone. Users are strongly encouraged to use full sentences with multiple words.


Text encoding

This specification uses the following exact encodings:

public key     32 bytes, encoded as 64 lowercase hexadecimal characters
               ([0-9a-f]), no prefix, no separators.
record id      32 bytes (SHA-256 output), encoded as 64 lowercase
               hexadecimal characters.
signature      64 bytes (Ed25519 output), encoded as standard base64
               per RFC 4648 section 4, with '=' padding, 88 characters total.

Implementations MUST use these exact forms. Uppercase hex, url-safe base64, or base64 without padding are all invalid.


Key derivation

Given a valid name and an incantation:

name_bytes  = UTF-8 bytes of name (ASCII)
inc_bytes   = bytes of incantation (ASCII, exact)
salt        = SHA-256("iam:v1:name:" || name_bytes)[0:16]
              // 16 bytes = 128 bits, conforming to RFC 9106 salt recommendation
master_seed = Argon2id(
                password    = inc_bytes,
                salt        = salt,
                memory      = 65536 KiB (64 MiB),
                iterations  = 3,
                parallelism = 1,
                tag_length  = 32
              )
root_seed   = HKDF-SHA256(
                ikm    = master_seed,
                salt   = empty,
                info   = "iam/v1/root-ed25519",
                length = 32
              )
root_key    = Ed25519.keypair_from_seed(root_seed)

The derivation is deterministic. The same name and incantation MUST always yield the same root key on any compliant implementation.

Argon2id parameters are fixed for iam-core 1.0. Changing them would create a different identity space and MUST be done only through a new IAM version with a new salt prefix (for example iam:v2:name:).

Note on parallelism: RFC 9106 recommends p=4 for the 64 MiB / 3-iteration profile. IAM uses p=1 as a deliberate trade-off to ensure derivation works on weak devices (phones, Raspberry Pi, older laptops) where allocating four parallel 64 MiB buffers is impractical. The reduction in parallelism slightly weakens resistance to offline brute force compared to the RFC 9106 default.

Security boundary

The security of an IAM identity is bounded by the unpredictability of the incantation. Argon2id provides resistance to offline brute force, but cannot make a weak phrase strong. An attacker who knows or guesses a target’s name (which is public) can attempt offline derivation against the published public key without rate limiting, without network interaction, and without observable activity.

This is a structural property of the design, not a bug. IAM identities are meant to be reproducible from human memory on any device without server interaction. Deterministic derivation is the price of that property.

Implementations and users MUST treat the incantation as a high-value secret:


Subkey derivation

Additional keys for specific purposes are derived from master_seed via HKDF-SHA256. The root Ed25519 key is one such derivation:

purpose_seed = HKDF-SHA256(
                 ikm    = master_seed,
                 salt   = empty,
                 info   = "iam/v1/" || purpose_name,
                 length = 32
               )

Implementations MUST use "iam/v1/root-ed25519" as the info string for the root signing key. Other purposes (encryption keys, device delegation, session keys) MAY use other info strings under the same scheme.


Seal

The seal is derived from the public key using the canonical wordlist:

pub_hash = SHA-512(public_key)
seal[i]  = WORDLIST[pub_hash[i]]  for i in 0..3

The wordlist is fixed by this specification (see appendix). It contains exactly 256 words, indexed from 0 to 255. Implementations MUST use this exact wordlist. Changing a single word creates an incompatible seal space.

Four words from 256 give 32 bits of fingerprint. This is sufficient for visual recognition, not sufficient for global uniqueness.


Fractal structure

An identity exists within two kinds of trust structure that share the same rules:

personal graph    devices owned by one identity
community graph   identities accepted by other identities

Both use the same record types, the same validation, and the same signatures. They are distinguished by the context field on every record.

An identity MAY participate in multiple community graphs simultaneously. Each graph is independent. Each community graph is identified by its tree id, which is a hash of its bootstrap list (see Genesis section). A personal graph is identified by the owner’s root public key.

Personal graph

alice (root_key, implicit root)
  |
  +-- laptop
  +-- phone
  +-- jan-home
  +-- jab-browser

Each device has its own Ed25519 keypair generated locally. The device private key MUST NOT be transmitted. The device public key is transmitted via the ACCEPT record that places the device in the personal graph.

The root key signs structural operations (ACCEPT and REVOKE of devices). Device keys sign session traffic and ordinary messages defined by JAM runtime. This separation means the incantation must be entered only when the graph structure changes, not during normal use.

Implicit root: For context = "personal", the public key equal to tree is the implicit root of that personal graph. It is valid by definition and requires no ACCEPT record. All other members of the personal graph MUST have a valid lineage to this implicit root.

No LEAVE in personal: LEAVE records in context = "personal" MUST be rejected. An identity cannot leave its own personal graph. Devices are removed via REVOKE signed by the root; the root itself exists as long as the identity exists.

Community graph

tree: <tree_id from bootstrap list>

bootstrap root members:  alice, bob   (axiomatically valid)

alice
  +-- charlie
  +-- dave

bob
  +-- eve
  +-- charlie   (also accepted by bob, multiple parents)

Bootstrap root members are axiomatically valid: they are named in the bootstrap list, and that naming is their claim to membership. They have no parent in the graph. Members below them are accepted by existing valid members via ordinary ACCEPT records. A member MAY have multiple parents (as charlie above, accepted by both alice and bob).

Every community graph is identified by the SHA-256 hash of its bootstrap list, not by the genesis public key. See the Genesis section for the bootstrap construction and why the tree id is content-bound to its bootstrap.


Authority

All structural records (ACCEPT, REVOKE, LEAVE) MUST be signed by the actor’s root key.

Device keys MUST NOT sign structural records. Device keys sign only session authentication and ordinary messages as defined by JAM runtime.

This rule is strict in iam-core 1.0. There is no delegation from root to device for structural operations. When a user adds or removes a device, or performs a graph operation, the incantation must be present to recompute the root key. Structural changes are deliberate and rare.

Device signature verification

A device signature carries authority only if the verifier possesses a valid, unrevoked personal ACCEPT chain from that device key to the claimed root key.

How personal graph records are distributed to verifiers is a runtime concern and is not specified by IAM.

Note on stale device acceptance: a device REVOKE issued by an owner takes effect for any verifier only after the verifier has received and admitted the REVOKE record. A revoked device may still appear valid to verifiers who have not yet received the REVOKE. This is the standard limitation of any distributed revocation system. Runtime layers SHOULD use mechanisms such as short-lived signatures, active replication policies, and periodic re-verification to bound the window of stale acceptance. IAM does not specify these mechanisms.


Compromise and recovery

If a root key is compromised, recovery within iam-core 1.0 is not possible at the protocol level. The compromised identity is structurally tied to its derivation from name and incantation; there is no mechanism to rotate the underlying root key while preserving membership.

This is a deliberate constraint of the constitution. Key rotation introduces complex semantics about which signatures remain valid across rotation events, which records are bound to which key, and how transitively trusting parties learn about the rotation. iam-core 1.0 does not attempt these.

Practical recovery requires social action:

  1. The compromised user creates a new identity (a different name, a new incantation, or both). The new identity has a new root key, a new seal, and no prior history.
  2. Existing community members re-ACCEPT the new identity into their portion of the graph.
  3. The compromised identity SHOULD be REVOKEd by any of its parents in each affected community graph. If the user retains control of the compromised key alongside the attacker, they MAY also issue LEAVE.
  4. All lineage to the compromised identity becomes invalid; lineages must be rebuilt from the new identity.

This is intentionally heavy. Compromise of a root key SHOULD be a rare event, treated with the same gravity as the loss of a long-held cryptographic key in any other system. Users who anticipate frequent rotation needs are using IAM for a use case it was not designed for.

Future IAM versions MAY add key rotation primitives. Runtime layers MAY implement rotation through composition of existing primitives plus runtime metadata, without modifying the constitution.

Compromise of a bootstrap root member

Compromise of a bootstrap root member key is the most severe failure mode in a community tree. Because bootstrap root members cannot be REVOKEd by any other member, and cannot be structurally removed from the axiomatic set except through their own LEAVE, a compromised bootstrap root member is effectively uncontainable within the tree.

If the attacker holds the key, they can refuse to LEAVE and continue signing ACCEPTs indefinitely. The community only recourse is to abandon the compromised tree and create a new tree with a new bootstrap list that excludes the compromised member. The old tree continues to exist as a historical artifact but receives no further social recognition.

This is a strong argument for using multiple bootstrap root members rather than a single point of compromise, and for treating bootstrap root member keys with the same operational gravity as genesis itself.


Records

Three record types define all changes to a graph:

ACCEPT    actor accepts target as a new child in (tree, context).
          target may already have other parents; ACCEPT adds actor as
          an additional parent.
REVOKE    actor removes the parent-child edge from actor to target.
          target's lineage through actor is broken; other lineages
          through other parents (if any) remain intact.
LEAVE     actor removes itself from (tree, context).
          all edges incident to actor (both incoming and outgoing)
          become inactive. if actor was an axiomatic member, that
          status is removed. departure is permanent within the
          evaluation. not valid for context = personal.

All three are unilateral: a single actor signature suffices. There are no handshakes, no bilateral records, no co-signatures in iam-core 1.0.

Canonical record form

A record is a JSON object with the following fields, serialized using JSON Canonicalization Scheme (RFC 8785). The input to JCS MUST be valid I-JSON (RFC 7493). Implementations MUST reject any record whose JSON representation contains duplicate member names within an object.

{
  "v":       1,
  "kind":    "ACCEPT" | "REVOKE" | "LEAVE",
  "tree":    <64 lowercase hex chars>,
  "context": "personal" | "community",
  "m":       <integer>,
  "actor":   <64 lowercase hex chars>,
  "target":  <64 lowercase hex chars>,
  "prev":    <64 lowercase hex chars or empty string>,
  "body":    {}
}

Field meanings:

Record id

id = lowercase_hex(SHA-256("IAM1:id\0" || JCS(record)))

where JCS is the canonical JSON serialization of the record object (before any signature is added).

Signature

sig = Ed25519.sign(
        key = actor_root_key,
        msg = "IAM1:record\0" || JCS(record)
      )

The signature is encoded as 88-character standard base64 (RFC 4648 section 4, with padding).

The complete stored form of a record is:

{
  "record": <canonical record>,
  "id":     <64 lowercase hex chars>,
  "sig":    <88-char base64 signature>
}

Implementations MUST verify the record against its id and signature before accepting it into any evaluation.


Inputs to evaluation

IAM evaluation requires two distinct inputs, which together describe everything an implementation needs to compute a graph state:

known_trees   a registry of verified bootstrap lists, one per tree id
records       a set of signed IAM records

Both inputs pass through a pipeline of four layers: observation, admission, anchoring, and evaluation.

known_trees registry

known_trees is a map from tree id to an ordered bootstrap list:

known_trees: { tree_id -> ordered_bootstrap_list }

A tree id is in known_trees only if:

  1. The corresponding bootstrap list has been observed in full.
  2. The bootstrap list has been verified: all Bootstrap constraints pass, every signature verifies, and the computed tree id matches.

For context = "personal", there is no bootstrap and therefore no entry in known_trees. Personal graphs are implicitly known by their owner’s root public key, which serves as the tree field.

known_trees is the set of community graphs this implementation recognizes. A record with context = "community" and tree = X can be admitted only if X is in known_trees.

Three layers for records

In addition to known_trees, records themselves pass through three layers. Implementations MUST NOT conflate them.

Observation

The set of records an implementation has seen via any transport. This is a runtime concern; IAM does not specify how records are obtained, distributed, or discovered. Observed records may include duplicates, malformed entries, records from unrelated trees, and entries received in arbitrary order.

Admission

Admission is the local policy by which an implementation moves a record from observation into its local store. Admission uses the implementation’s wall clock and is necessarily a local decision.

Admission has two tracks: one for bootstrap lists and one for ordinary records.

Bootstrap admission. A bootstrap list is admitted into known_trees if:

  1. All bootstrap records pass structural validity (with rule 6 relaxed for bootstrap records, see Bootstrap constraints).
  2. All bootstrap-specific constraints pass.
  3. The computed tree id from the reduced descriptor form matches the tree field carried by all records in the list.
  4. Every signature verifies against the shared actor (genesis public key).

A verified bootstrap list is added to known_trees under its tree id. A bootstrap list that fails any check is rejected.

Record admission. An ordinary (non-bootstrap) record is admitted if:

  1. Structural validity passes (see Structural validity below).
  2. For context = "community", tree is already in known_trees. A record referencing an unknown tree MAY be held in quarantine pending bootstrap admission for that tree.
  3. m <= now + 5 where now is the implementation’s current clock in minute units. Records with m further in the future are held in quarantine and MAY be retried as time advances.
  4. The implementation has not already admitted a record with the same id (replay protection).
  5. prev either equals the empty string or equals the id of a record already admitted for the same (tree, context, actor). Records with unknown prev MAY be held in quarantine pending arrival of the predecessor.

Admission is local. Two implementations may admit the same record at different times. Admission is NOT canonical evaluation. Two implementations with identical admitted sets but different anchored sets MAY compute different graph states; that is correct and expected.

Anchoring

The anchored set is the subset of admitted records over which canonical evaluation runs. The anchored set is chosen by the implementation, the operator, or the community using runtime-defined policy. IAM does not specify how anchoring decisions are made.

An anchored set MUST be closed under prev: for every record r in the set, if r.prev is non-empty, then the record with id equal to r.prev MUST also be in the set.

An anchored set is evaluated together with the current known_trees registry. For any record r in the anchored set with context = "community" and tree = X, the tree id X MUST be present in known_trees. An anchored set that references an unknown tree is incomplete and MUST cause evaluation to abort with an error; the implementation should first admit the missing bootstrap list before attempting evaluation. Returning an empty state is NOT a valid response to an unknown tree.

This rule makes bootstrap loss impossible within any live evaluation: evaluation cannot proceed without the bootstrap being in known_trees, and known_trees is populated only by verified bootstrap lists. Bootstrap replication is a prerequisite of holding graph state, not a separate concern.

Anchoring separates transport from truth. A community recovering from a fork can choose an anchored set that excludes one branch. A new joiner can adopt an anchored set provided by a trusted peer (typically including both the bootstrap list for known_trees and a set of records). A historical audit can replay a graph at a past anchor. None of these change what was observed or admitted; they change what is considered authoritative for evaluation.


Canonical evaluation

Canonical evaluation is a pure function of its two inputs: the known_trees registry and an anchored record set. It MUST NOT depend on wall-clock time, arrival order, network topology, or any local state beyond these inputs.

evaluate(known_trees, anchored_set, tree, context) -> graph state

Given the same known_trees and the same anchored record set, any two implementations MUST compute identical graph state. This is the determinism guarantee of IAM.

For the evaluation to be well-defined, known_trees[tree] MUST exist when context = "community". If context = "personal", known_trees is not consulted; the implicit root is derived directly from the tree field.

Structural validity

A record is structurally valid if:

  1. v equals 1.
  2. kind is one of ACCEPT, REVOKE, LEAVE.
  3. context is one of personal, community.
  4. tree is a valid 64-character lowercase hex string.
  5. For context = personal, tree equals actor (the implicit root is the owner).
  6. For context = community, tree is present in known_trees (the tree has been admitted, its bootstrap list verified).
  7. m is a non-negative integer.
  8. actor and target are valid 64-character lowercase hex public keys.
  9. For ACCEPT, actor != target.
  10. For LEAVE, target equals actor.
  11. For LEAVE with context = personal, the record is REJECTED.
  12. prev is either the empty string or a 64-character lowercase hex id.
  13. If prev is non-empty, the record referenced by prev (when available during evaluation) MUST have the same (tree, context, actor) as this record. Evaluation rejects records whose prev points outside their own chain.
  14. body equals {}.
  15. id matches SHA-256 of the canonical serialization.
  16. sig verifies against actor over the domain-separated canonical bytes.

A record failing any of these checks is rejected and does not participate in evaluation.

Chain monotonicity

Within the same (tree, context, actor), m MUST be non-decreasing along the prev chain:

record.m >= prev_record.m

A record whose m is strictly less than the m of its prev is rejected. Equal m values are permitted because ordering within a chain is already determined by prev.

Fork detection

If two distinct records from the same (tree, context, actor) share the same prev value, the actor’s chain in that (tree, context) has forked. Both records remain individually signed, but the chain is considered broken from the fork point forward.

Implementations MUST NOT pick a winner based on m, id, or any other field. Both records exist as evidence of misbehavior. No further records from the same actor in the same (tree, context) that depend on either fork branch are valid.

Recovery from a fork is handled at the anchoring layer, not within canonical evaluation. The community or operator chooses an anchored set that excludes the fork (typically by omitting one of the conflicting records and everything causally downstream of it). Within any anchored set that contains both fork records, the affected actor’s chain is broken from the fork point and remains broken; canonical evaluation has no notion of repair.

Multi-parent acceptance

A target MAY be accepted by multiple actors in the same (tree, context). Each ACCEPT record creates an independent parent-child edge. The target has as many parents as there are active (non-revoked) ACCEPT records pointing to it.

Each parent-child edge is independent. REVOKE by one parent breaks only that edge. The target remains a member of the graph as long as at least one parent edge remains and a valid lineage exists from that parent back to an axiomatic member (a bootstrap root member for community, or the implicit root for personal).

This makes the structure a directed acyclic graph, not strictly a tree. Cycles are prevented by the rule below.

Cycle prevention

An ACCEPT record is rejected if it would create a cycle: if actor is itself a descendant of target in the current (tree, context) state, the ACCEPT is invalid.

This check is performed against the evaluated state before the ACCEPT is applied. If the ACCEPT would make target an ancestor of actor (directly or transitively), it is rejected.

ACCEPT validity

Beyond structural validity, an ACCEPT is semantically valid if:

  1. actor != target. A member cannot ACCEPT themselves.
  2. actor is a currently valid member of (tree, context) at evaluation time.
  3. actor is not in state.departed. A member who has LEFT cannot issue ACCEPTs in the same evaluation.
  4. target is not in state.departed. A member who has LEFT cannot be re-accepted within the same evaluation. Departure is permanent within a single evaluation.
  5. The ACCEPT would not create a cycle (see above).
  6. No active ACCEPT from the same actor to the same target in the same (tree, context) already exists. Re-ACCEPTing after REVOKE is permitted; duplicate active ACCEPT is not.

A revoked or departed actor MUST NOT issue valid ACCEPTs. An actor whose membership was lost (because they were revoked, they LEFT, or the chain providing their membership was broken) cannot vouch for new members until they themselves are re-accepted.

Bootstrap records are a special case. Bootstrap records are not ordinary ACCEPT records subject to this validity rule. They are processed separately in Phase 0 of canonical evaluation and validated against Bootstrap constraints (see Genesis section). The genesis public key is not a member of the graph; it is the ephemeral signer of the bootstrap, nothing more. Bootstrap records do not create parent-child edges in the graph — they directly establish their targets as axiomatic members (bootstrap root members) of the tree.

Personal context. For context = personal, the implicit root is axiomatically valid. The implicit root is the public key whose value equals the tree field (which in personal context is itself a public key, the owner’s root pubkey). The implicit root may ACCEPT any number of devices. All non-root members of a personal graph must have a valid lineage back to the implicit root.

Note on phantom acceptance: ACCEPT is unilateral. The target does not consent to being accepted and may not be aware of it. A valid actor may ACCEPT any public key, including keys whose owners are unrelated to the graph, do not exist, or did not authorize the action. Such phantom acceptances are structurally valid but practically inert: a phantom member cannot sign records (no one holds the private key), cannot vote, cannot participate, and cannot REVOKE or LEAVE. Phantom members appear in the graph topology but contribute nothing. Runtime layers MAY filter or hide phantom members based on activity criteria; iam-core does not.

REVOKE validity

A REVOKE is semantically valid if:

  1. actor is a currently valid member of (tree, context) at evaluation time. A revoked or departed actor MUST NOT issue valid REVOKEs over their past edges.
  2. An active ACCEPT from actor to target exists in (tree, context) at evaluation time.
  3. actor != target.

REVOKE breaks only the specific parent-child edge from actor to target. Other edges to target (from other parents, if any) remain. If target has no remaining active parent edges and is not an axiomatic member of the graph, target loses membership.

Descendants of target that depended exclusively on the lineage through this revoked edge also lose their valid lineage. Descendants with alternative lineages through other parents remain valid.

Bootstrap root members cannot be revoked within their own tree. Because bootstrap records do not create parent-child edges (they establish axiomatic membership instead), there is no ACCEPT edge pointing at a bootstrap root member within the tree where they are axiomatic. Therefore no actor within that tree can satisfy REVOKE validity condition 2 against a bootstrap root member. This is a structural property of Model B, not an oversight.

A bootstrap root member can exit the graph in exactly two ways:

Compromise of a bootstrap root member’s key is therefore a severe event: the compromised member cannot be REVOKEd by any other member. The only remedies are self-LEAVE (which requires the compromised key to cooperate), or creation of a new tree. Implementations and operators SHOULD treat bootstrap root member key compromise with maximum gravity.

LEAVE validity

A LEAVE is semantically valid if:

  1. context = community (LEAVE in personal is rejected).
  2. actor is a currently valid member of (tree, context) at evaluation time, either through an active ACCEPT lineage or through axiomatic status.
  3. target = actor.

LEAVE removes the actor from the active graph. Three simultaneous effects occur:

After a valid LEAVE, the actor is no longer a member by any path.

Children of actor lose any lineage that passed through actor; they retain any other lineages they may have through other parents. Members who had accepted actor (parents of actor) lose that specific edge to actor; their own position in the graph is otherwise unchanged.

LEAVE is the only way a bootstrap root member can exit their own tree. Because bootstrap root members have no parent edge, no other member can REVOKE them. LEAVE by the bootstrap root member themselves is the only protocol-level mechanism for their removal from the axiomatic set.

Membership evaluation algorithm

Canonical evaluation is a pure function:

evaluate(known_trees, anchored_set, tree, context) -> graph state

The anchored set is provided externally (see Anchoring section). It MUST be closed under prev: for every record r in the anchored set, if r.prev is non-empty, then the record with id equal to r.prev MUST also be in the set.

Evaluation is a topological traversal over prev chains, with (m, id) ordering as a deterministic tiebreaker among records that are simultaneously eligible. The algorithm makes every check explicit.

Before the main loop, two preflight checks run on the anchored set:

// Preflight 1: anchored set must not contain bootstrap records
for each r in anchored_set:
    if r.tree in known_trees AND r is in known_trees[r.tree]:
        ABORT: anchored set contains a bootstrap record

// Preflight 2: anchored set is filtered to the requested graph
scope := { r in anchored_set :
           r.tree == tree AND r.context == context }

All subsequent work operates on scope, not on the full anchored set. Bootstrap records are NOT in scope, because they MUST NOT be in anchored_set in the first place. They are read exclusively from known_trees[tree] in Phase 0.

Bootstrap records MUST NOT appear in anchored_set. They live exclusively in known_trees. An implementation that finds a bootstrap record in an anchored set MUST abort evaluation and treat the anchored set as malformed. The two data structures are disjoint by construction: known_trees holds bootstrap lists indexed by tree id, anchored_set holds ordinary records that reference those tree ids via their tree field.

The graph state is a structure with at least:

state.edges          active ACCEPT edges (parent, child)
state.axiomatic      set of currently active axiomatic members
state.departed       set of members who have LEFT (for LEAVE of target check)

The algorithm is:

state := empty graph state
processed := empty set
broken_chains := empty set     // (tree, context, actor) tuples

// Phase 0: bootstrap (community context only)
if context == "community":
    if tree not in known_trees:
        ABORT: anchored set references an unknown tree
    bootstrap := known_trees[tree]  // ordered bootstrap list
    for each r in bootstrap (in bootstrap list order):
        state.axiomatic := state.axiomatic + { r.target }
    // Note: bootstrap records are NOT in the anchored set's scope
    //       and are NOT subject to main-loop processing. They
    //       establish axiomatic membership directly. The genesis
    //       public key is never a node in the graph.

// For personal context, the implicit root is axiomatic
if context == "personal":
    state.axiomatic := state.axiomatic + { tree }   // tree == root pubkey

// Phase 1: topological traversal of non-bootstrap records
non_bootstrap := scope - processed
ready := { r in non_bootstrap : r.prev = "" OR
                                anchored_set[r.prev] in processed }

while ready is not empty:
    r := record in ready with smallest (m, id)
    ready := ready - { r }
    chain_key := (r.tree, r.context, r.actor)

    // Always enqueue children BEFORE any rejection path,
    // so the traversal reaches every reachable record.
    for each c in non_bootstrap with c.prev == r.id:
        if c not in processed and c not in ready:
            ready := ready + { c }

    // Step 1: structural validity
    if not structurally_valid(r):
        processed := processed + { r }
        continue

    // Step 2: prev chain integrity (for non-bootstrap records)
    if r.prev != "":
        if r.prev not in anchored_set:
            processed := processed + { r }
            continue
        prev_record := anchored_set[r.prev]
        if prev_record.tree != r.tree OR
           prev_record.context != r.context OR
           prev_record.actor != r.actor:
            processed := processed + { r }
            continue
        // Step 3: monotonicity along prev chain
        if r.m < prev_record.m:
            processed := processed + { r }
            continue

    // Step 4: chain not already broken by an earlier fork
    if chain_key in broken_chains:
        processed := processed + { r }
        continue

    // Step 5: fork detection
    // (any other record in non_bootstrap with same chain_key
    //  and same prev value as r constitutes a fork)
    siblings := { s in non_bootstrap :
                  s != r AND
                  (s.tree, s.context, s.actor) == chain_key AND
                  s.prev == r.prev }
    if siblings is not empty:
        broken_chains := broken_chains + { chain_key }
        processed := processed + { r }
        continue

    // Step 6: semantic validity against current state
    if not semantically_valid(r, state):
        processed := processed + { r }
        continue

    // Step 7: apply
    if r.kind == ACCEPT:
        state.edges := state.edges + active_edge(r.actor, r.target)
    elif r.kind == REVOKE:
        mark edge (r.actor, r.target) inactive in state.edges
    elif r.kind == LEAVE:
        // Break all edges where r.actor is either endpoint
        mark all edges incident to r.actor inactive in state.edges
        // If r.actor is an axiomatic member, remove that status
        state.axiomatic := state.axiomatic - { r.actor }
        // Record the departure so that LEAVE-of-target checks
        // can invalidate edges where the former target has left
        state.departed := state.departed + { r.actor }

    processed := processed + { r }

return state

Notes on the algorithm:

Two implementations running this algorithm on the same anchored set MUST produce identical state for the same (tree, context).

Why prev rather than (m, id) alone: a single actor in a single (tree, context) can produce multiple records in the same minute. The prev chain determines causal order within an actor. If two records share the same prev, that is a fork (see Fork detection) and the chain is broken from that point.

Why (m, id) as tiebreaker: when records from different actors become eligible simultaneously (their respective prev records have all been processed), some deterministic order is needed. (m, id) ascending provides it. This ordering does NOT pick winners in conflicts; conflicts are handled explicitly by fork detection and multi-parent acceptance.

A member is considered present in the graph state if any of the following holds:

Axiomatic members are the base case of membership. A bootstrap root member is initially in state.axiomatic after Phase 0. If they LEAVE, they are removed from state.axiomatic and added to state.departed. Within the same evaluation, departure is permanent: a departed member cannot be re-accepted (see ACCEPT validity rule 4). A different anchored set (one that excludes the LEAVE record, by social or operational decision at the anchoring layer) would yield a different state in which the member is still axiomatic. This is the only way “return” is meaningful, and it is a property of anchoring, not of evaluation.

All other (non-axiomatic) members are derived by ACCEPT chains leading back to at least one currently active axiomatic member. Neither tree nor the genesis public key are themselves nodes in the graph; they are identifiers. Genesis signs the bootstrap list and is then destroyed; it never exists as a member.


Lineage and membership

These are two distinct concepts. Implementations MUST NOT conflate them.

Lineage

A lineage is a chain of active ACCEPT records from a member back to an axiomatic member. Because a member may have multiple parents, a member may have multiple lineages. Each lineage proves one path of origin.

charlie <- accepted by alice   (alice is an axiomatic bootstrap root member)
charlie <- accepted by bob     (bob is also an axiomatic bootstrap root member)
                               (charlie has two lineages)

Lineage verification walks each ACCEPT record in turn and checks its signature, then confirms that the lineage terminates at an axiomatic member of the tree. Verification of one lineage is O(depth) Ed25519 operations plus one bootstrap membership lookup.

Membership

Membership is the evaluated state of the graph at a specific (known_trees, anchored_set) pair. A member is currently present in the graph if:

  1. The member is an axiomatic member of the tree (a bootstrap root member for community, or the implicit root for personal), OR
  2. At least one complete chain of active ACCEPT records exists from the member to some axiomatic member, with every link in that chain itself currently valid by the same recursive definition.

An active ACCEPT is one that has not been broken by any of the following:

When either endpoint of an edge leaves the graph, the edge becomes inactive.

Lineage proves that a path of origin existed at some point. Membership proves that at least one such path is currently intact.


Time

m = floor(unix_seconds / 60) - 28928160

One tick is one minute. Epoch is 2025-01-01 00:00 UTC.

Every record carries m. m provides chain monotonicity, freshness bounds during admission, and deterministic evaluation order. Implementations MUST use integer minute granularity.

Note: m is set by the signer and cannot be trusted as an objective time source for conflict resolution. IAM uses m only for chain ordering (where prev already establishes causality) and freshness checks during admission. m is NOT used to pick winners in forks or multi-parent situations.

Cross-actor causality is not protected by prev

Prev chains establish causality within a single actor’s sequence of records, but IAM has no direct mechanism to express “record R depends on an earlier record S from a different actor”. If the validity of a record depends on the prior acceptance of the actor by another member, that ordering is enforced only through the (m, id) deterministic tiebreaker among simultaneously eligible records.

For example: Alice ACCEPTs Bob, then Bob ACCEPTs Charlie. If Alice’s and Bob’s records both have the same m, the evaluation algorithm processes them in (m, id) order. If Bob’s record is processed before Alice’s (because Bob’s id happens to sort earlier), Bob is not yet a valid member when his ACCEPT is evaluated, and his record is rejected.

This is deterministic (every implementation reaches the same result) but it is a sharp edge. To avoid it, records whose validity depends on an earlier record by another actor SHOULD have m strictly greater than the m of that dependency. Runtime layers MAY enforce this by delaying publication of dependent records, or by rejecting attempts to submit records with insufficient m.

IAM does not attempt to enforce this constraint structurally, because doing so would require tracking inter-actor causal links, which is not part of the record format. Implementations and users are expected to respect monotonic wall-clock time in practice.


Genesis

A community graph begins with genesis. Genesis produces the bootstrap list: the ordered sequence of initial ACCEPT records that seed the graph’s first root members. The bootstrap list is the eternal anchor of the graph. Order is part of the identity of the graph: two bootstrap lists with the same records in different order produce different tree ids and are different graphs.

The term “bootstrap list” is used throughout this specification in preference to “set”, because ordering is significant. Implementations MUST preserve the order of bootstrap records as published.

Three concepts

Three related but distinct concepts appear in the community graph and MUST NOT be confused:

Personal graphs have a simpler structure: only tree and an implicit root, both equal to the owner’s Ed25519 root public key. The implicit root IS a member of the personal graph.

Bootstrap creation

A community graph is bootstrapped by an atomic batch operation: genesis signs an ordered set of ACCEPT records that seed the initial root members. All bootstrap records share the same m, all have prev = "", and their order is part of the tree identity.

Bootstrap records do NOT form an internal prev chain. They are parallel, not sequential. Their order is determined by their position in the bootstrap list, which is itself the input to the tree id hash. This avoids the circular dependency that would otherwise arise (record id depends on tree, tree depends on bootstrap content, bootstrap content includes record ids).

To create a new community graph:

1. Generate 32 random bytes as a seed. These bytes MUST come from a
   cryptographically secure random source. They MUST NOT be derived
   from any incantation or other recoverable value.
2. root_seed = HKDF-SHA256(ikm=seed, salt=empty,
                           info="iam/v1/root-ed25519", length=32)
3. genesis_key = Ed25519.keypair_from_seed(root_seed)
4. Decide the ordered list of initial root members
   [target_1, target_2, ..., target_n]. Order is part of the
   bootstrap and is preserved through the tree id hash.
5. Choose a single bootstrap timestamp m_b (current m value).
6. For each target_i in order, construct a bootstrap descriptor
   (a reduced record form, used only for tree id computation):
      desc_i = {
          "v":       1,
          "kind":    "ACCEPT",
          "context": "community",
          "m":       m_b,
          "actor":   genesis_key.pub,
          "target":  target_i
      }
   Note: desc_i has neither tree, prev, body, id, nor sig.
7. Compute the tree id from the ordered descriptor list:
      tree_id = lowercase_hex(
          SHA-256("IAM1:tree\0" || JCS([desc_1, desc_2, ..., desc_n]))
      )
8. For each i, construct the full bootstrap record by adding
   tree, prev, and body to desc_i:
      record_i = desc_i + {
          "tree":  tree_id,
          "prev":  "",
          "body":  {}
      }
9. Compute id and sig for each full bootstrap record:
      id_i  = lowercase_hex(SHA-256("IAM1:id\0" || JCS(record_i)))
      sig_i = Ed25519.sign(genesis_key,
                           "IAM1:record\0" || JCS(record_i))
10. The seed and private key MUST be erased from memory immediately
    after all bootstrap records have been signed.
11. The bootstrap list is published as the graph's permanent
    anchor. The published form includes tree_id, ids, signatures,
    and the original ordering of records.

The tree id is computed from a reduced descriptor form, not from the full records. This is the only place in IAM where a hash is computed over something other than a complete canonical record. The reduction is necessary and sufficient: it captures everything that defines the bootstrap (genesis pubkey via actor, root members via target, batch time via m, and order via JCS array order) while excluding fields that would create circular dependencies (tree, prev, id, sig).

Bootstrap constraints

Every bootstrap record MUST satisfy all Structural validity rules (see Canonical evaluation section), with one exception:

All other structural validity rules apply unchanged: v = 1, valid hex lengths, actor != target, valid signature over the domain-separated canonical form, and so on.

In addition to structural validity, a valid bootstrap list MUST satisfy the following bootstrap-specific constraints:

  1. The list contains at least one record (non-empty).
  2. Every record has kind = "ACCEPT" and context = "community".
  3. All records share the same actor value (the genesis public key).
  4. All records share the same m value (the bootstrap timestamp).
  5. All records have prev = "" and body = {}.
  6. All records share the same tree field, equal to the computed tree id.
  7. The target values are pairwise distinct. No member appears twice in the bootstrap list.
  8. Every record’s signature verifies against the shared actor (genesis public key).
  9. The actor value does not appear as any target (genesis does not accept itself).

A bootstrap list that fails any structural validity rule or any bootstrap-specific constraint is invalid and produces no valid tree. Implementations MUST verify all constraints before accepting a tree as known.

The relaxation of structural validity rule 6 is the only privilege bootstrap records receive. In every other respect, they are ordinary records that happen to be signed by an ephemeral key.

Genesis has no human. No incantation ever existed. The private key lives in one machine’s memory only long enough to sign the bootstrap records. After erasure, genesis cannot sign anything else. It is dead by construction.

Implementations SHOULD expose this process as a single command (for example iam genesis). The erasure in step 10 is MUST.

Tree identifier

The tree identifier for a community graph is a SHA-256 hash of the bootstrap content, not a public key. This matters:

Bootstrap records are valid by definition

Bootstrap records satisfy ACCEPT validity as a special case: they are the records that define the tree. The condition “actor is a currently valid member” is replaced by “the record is one of the bootstrap records for this tree”. This breaks the otherwise circular requirement that genesis itself be a member before accepting anyone.

Bootstrap is exempt from fork detection

Bootstrap records share the same (tree, context, actor, prev) tuple by design: they all have context = "community", the same tree (the tree id they define), the same actor (genesis), and prev = "". Under the general Fork detection rule, this would classify the bootstrap as a forked chain and invalidate every bootstrap record.

Bootstrap is an explicit exception to fork detection. The rule is:

Fork detection does NOT apply to records belonging to a bootstrap
list. A record is a bootstrap record for tree X if and only if
it is part of the bootstrap list whose computed tree id equals X.

Bootstrap records are instead validated as an atomic batch: either the entire bootstrap list is valid (all constraints met, tree id matches, all signatures valid) or the entire list is rejected. There is no partial bootstrap.

After the bootstrap batch is validated, subsequent records from any actor (including genesis, if it were somehow to sign more records — which it cannot, because its private key has been erased) follow the normal Fork detection rules. But since genesis cannot sign anything after erasure, its chain effectively ends with the bootstrap batch.

A record is a bootstrap record for tree X if and only if it appears in the bootstrap list whose computed tree id equals X. To verify a claimed bootstrap list:

1. Strip each record to its descriptor form (v, kind, context, m,
   actor, target only).
2. Verify that all records share the same m (the bootstrap timestamp).
3. Verify that all records have prev = "" and body = {}.
4. Compute SHA-256("IAM1:tree\0" || JCS([desc_1, ..., desc_n])).
5. Compare the result with the claimed tree id.
6. Verify each full record's id and sig independently.

If all checks pass, the bootstrap list is authentic for the given tree id.

Bootstrap set immutability

Once published, the bootstrap list is immutable. Its tree id is computed from its content; any change produces a different tree id and therefore a different tree. All bootstrap records have prev = ""; they do not form a sequential chain among themselves.

Records that come after bootstrap follow the ordinary prev chain rules. The prev field is defined per-actor per-(tree, context), so the first record by any root member in a given (tree, context) has prev = "", regardless of whether bootstrap records exist for that tree. Bootstrap records belong to genesis’s chain, not to the root members’ chains; a root member’s own chain is independent and begins fresh at their first structural action.

Loss of bootstrap records

If all copies of the bootstrap records for a given tree id are lost, the tree is dead. No party can sign new records that would be valid, because no member can prove their lineage to a root that cannot be verified. Runtime layers are expected to replicate bootstrap records aggressively and persistently, but IAM does not mandate any particular replication scheme.

A tree is a living system. Living systems can die. Death of a tree is not a protocol failure; it is a natural outcome when its community fails to preserve its foundation. The remedy is to create a new tree (new genesis, new bootstrap, new tree id) and re-establish the community within it. The old tree remains as a historical fact; it simply has no evaluable state.

Personal graphs have no bootstrap

Personal graphs use tree = actor_root_pubkey. There is no bootstrap list and no genesis process for personal graphs. The implicit root is valid by definition as stated in the Fractal structure section.


Storage and transport are out of scope

IAM does not define how records are stored, how they are transported between nodes, or how nodes discover each other. These are JAM runtime concerns.

Common choices for storage include git repositories, append-only log files, embedded databases, and distributed hash tables. Common choices for transport include WebSocket, HTTP, and peer-to-peer overlays. IAM is agnostic to all of them.

What IAM requires of any host:

- Records are identified by their canonical id (SHA-256 of canonical JSON).
- Admission uses local clock for freshness; canonical evaluation does not.
- Canonical evaluation operates only on (known_trees, anchored_set) inputs.
- The prev field creates a per (tree, context, actor) hash chain.
- Signatures are verifiable against published public keys.
- Given identical (known_trees, anchored_set) inputs, evaluation produces identical state.

Any runtime satisfying these requirements is a valid IAM host.


What IAM does not define

The following concerns are explicitly outside iam-core 1.0 and belong to JAM runtime or higher layers:

Some of these may appear in future IAM versions. Most belong permanently to JAM runtime, where they can evolve independently of the constitution.


Sovereignty

IAM defines:

- identity creation (name, incantation, root key derivation, seal)
- graph identification (bootstrap hash for community, root pubkey for personal)
- graph operations (ACCEPT, REVOKE, LEAVE)
- record structure (canonical JSON, id, prev chain, signatures)
- record authority (root keys sign structural records, actor must be currently valid)
- three layers: observation, admission, anchoring
- canonical evaluation (deterministic function over known_trees and anchored_set)
- topological traversal (prev as causality, (m, id) as tiebreaker)
- validity (structural and semantic, including cycle prevention and fork detection)
- multi-parent acceptance (DAG, not strictly tree)
- time (m, granularity, admission bounds, NOT a conflict judge)
- lineage (origin path)
- membership (evaluated presence at anchored state)
- genesis (bootstrap list creation, tree id as content hash)
- bootstrap inclusion (anchored set MUST contain bootstrap for every referenced tree)
- mortality (trees can die if bootstrap is lost; this is accepted, not a failure)
- security boundary (offline brute force is bounded only by incantation entropy)
- compromise recovery (no key rotation; recovery is via new identity and re-acceptance)
- phantom acceptance (ACCEPT is unilateral; phantom members are inert)
- axiomatic member semantics (bootstrap root can self-leave but cannot be externally revoked)
- known_trees registry as first-class input to evaluation

Everything else belongs to the runtime. IAM is the constitution: small, boring, and nearly immutable.

IAM does not define bridges to or from external identity systems (OAuth, OIDC, SAML, WebAuthn, or any other). Such bridges are runtime concerns and may be implemented as needed without modifying IAM core. An IAM identity and an OAuth account may coexist in the same application; they are never merged into a single key.


Wordlist

The canonical IAM wordlist contains exactly 256 words, indexed 0 through 255. Implementations MUST use this exact list:

amber      anchor     apex       arch       ash        aspen      atlas      azure
bark       basin      beacon     beam       berry      birch      bison      blade
bloom      bolt       bone       boulder    brass      breeze     briar      bridge
brook      calm       canyon     cape       cedar      chain      chalk      cipher
clay       cliff      clock      cloud      coal       coast      cobalt     comet
coral      cove       crag       crane      creek      crest      cross      crown
crystal    curve      cypress    dagger     dale       dance      dawn       delta
depth      dew        dial       dome       dove       draft      drake      dream
drift      drum       dune       dusk       eagle      earth      east       echo
edge       elder      elm        ember      epoch      fable      falcon     fawn
feather    fern       field      finch      fire       fjord      flame      flare
flax       fleet      flint      flora      fog        forge      fossil     fox
frond      frost      gable      gale       garnet     gate       gem        ghost
glade      glass      glen       globe      gold       grain      granite    grove
guild      gull       gust       halo       harbor     hawk       hazel      heart
heath      helm       heron      hill       hive       hollow     honey      horizon
horn       hound      hush       ice        inlet      iron       isle       ivory
jade       jasper     jet        jewel      jungle     juniper    kelp       kindle
knoll      lake       lance      lark       latch      laurel     leaf       ledge
light      lilac      linden     loam       lodge      lotus      lunar      lynx
maple      marble     marsh      meadow     mesa       mint       mist       moon
moss       muse       myth       north      nova       oak        oasis      oat
onyx       orbit      orchid     otter      palm       path       peak       pearl
pine       plume      pond       prairie    prism      pulse      quartz     quill
quiver     rain       raven      reed       reef       ridge      river      robin
rock       root       rose       rune       rust       saber      sage       sand
shard      shell      shore      silk       silver     slate      smoke      snow
south      spark      spire      spring     star       steel      stone      storm
sun        surge      swift      thicket    thorn      tide       timber     tor
torch      trail      vale       vault      vine       violet     vista      void
wake       wave       west       wheat      wild       willow     wind       winter
wolf       wood       wren       yarn       yew        zenith     zephyr     zero

iam-core 1.0