Specification · Frozen · 2026-04-28

cf 0.30 Wire-Format Freeze Snapshot

Status: Frozen at cf 0.30 / cf-protocol 1.0 / cf-authority 1.0.
Purpose: Canonical surface enumeration for Phase 8 Gate 4. Tests in test/demo/20-wire-format-freeze.sh verify this snapshot against the upstream spec text.
Sources: docs/protocol-spec.md (L1), docs/cf-authority-spec.md (L3).
Item: campfireagent-529.


§1 — L1 cf-protocol Freeze

Layer 1 surfaces frozen at cf-protocol 1.0. No PR may alter these surfaces after the 0.30 tag without a major-version bump.

§1.1 Envelope Structure

Source: protocol-spec.md:147–156

The L1 message envelope is the canonical wire unit. Every message shares this structure:

Message {
  id: uuid                          # [verified] unique identifier
  sender: public_key                # [verified] must match signature key
  payload: bytes                    # [TAINTED] sender-controlled content
  tags: [string]                    # [TAINTED] sender-chosen labels (campfire:* verified separately)
  antecedents: [uuid]               # [TAINTED] sender-asserted causal claims, not proofs
  timestamp: uint64                 # [TAINTED] sender's wall clock, not authoritative
  signature: bytes                  # [verified] sender signs (id + payload + tags + antecedents + timestamp)
  provenance: [ProvenanceHop]       # [verified] each hop independently verifiable
}

The signed payload covers: id + payload + tags + antecedents + timestamp. These six fields are the signature inputs and cannot change position or type without breaking all existing signatures. The antecedents field is CBOR field 5 of the signed envelope — freezing the envelope freezes the antecedents wire binding.

§1.2 Signatures

Source: protocol-spec.md:154, 163–165, 188

All signatures are Ed25519. Two signing authorities:

Tags in the campfire: namespace (except the three member-signed exceptions: campfire:vouch, campfire:revoke, campfire:invite) MUST be signed by the campfire's own key. Receivers MUST reject any campfire:* message whose signature does not verify against the campfire's public key.

§1.3 Hop Chain (ProvenanceHop)

Source: protocol-spec.md:180–198

Each campfire that relays a message appends a hop:

ProvenanceHop {
  campfire_id: public_key              # [verified] must match hop signature key
  membership_hash: bytes               # [verified] Merkle root, independently resolvable
  member_count: uint                   # [verified] derivable from membership hash
  join_protocol: string                # [verified] campfire-asserted policy
  reception_requirements: [string]     # [verified] campfire-asserted policy
  timestamp: uint64                    # [verified] campfire-asserted (not sender-controlled)
  role: string                         # [verified] membership role at time of relay (omitted when empty)
  signature: bytes                     # [verified] campfire signs (message.id + all fields above)
}

The role field was added in protocol-spec.md v0.4. When omitted (empty string), it is not included in the CBOR encoding so that hops produced by pre-role implementations verify identically. A bridge sets role = "blind-relay" on forwarded hops.

All hop fields are verified: the campfire key signs message.id + campfire_id + membership_hash + member_count + join_protocol + reception_requirements + timestamp + role. No hop field may be forged by the original message sender.

§1.4 System Events

Source: protocol-spec.md:1126–1152

Pre-0.30 system events (frozen at P1)

TagSigned byIntroduced
campfire:member-joinedcampfire keyP0
campfire:member-evictedcampfire keyP0
campfire:member-leftcampfire keyP0
campfire:evictioncampfire keyP0
campfire:rekeycampfire key (OLD key)P0
campfire:disbandcampfire keyP0
campfire:invitemember key (exception)P0
campfire:vouchmember key (exception)P0
campfire:revokemember key (exception)P0
campfire:compactcampfire key (full role required)P1
campfire:viewcampfire key (full role required)P1
campfire:member-role-changedcampfire keyP1

0.30 additions (frozen at cf-protocol 1.0)

Source: protocol-spec.md:1144–1146, 1162–1212

TagSigned byIntroduced
campfire:visibility-changedcampfire key (NOT a member-signed exception)0.30
session:opencampfire key (NOT a member-signed exception)0.30
session:closecampfire key (NOT a member-signed exception)0.30

campfire:visibility-changed (protocol-spec.md:1165–1183):
Fires when a campfire transitions from open to invite-only. Campfire-key-signed. Receivers MUST treat as authoritative and invalidate cached join-policy state on receipt.

Field layout:

campfire:visibility-changed payload {
  previous_join_protocol: string   # "open" — the prior join policy
  new_join_protocol:      string   # "invite-only" — the new join policy
  changed_at:             uint64   # unix nanosecond timestamp of the change
}

session:open (protocol-spec.md:1185–1198):
Emitted by the session orchestrator when a new ephemeral session campfire is initialized. Campfire-key-signed.

Field layout:

session:open payload {
  session_id:                    string   # campfire ID of this session campfire
  parent_grant_chain_root:       string   # hex-encoded public key of the chain root (human)
  dispatcher_capability_template: object  # capability template intersected with worker grants
  until:                         uint64   # session expiry in unix nanoseconds
}

session:close (protocol-spec.md:1200–1210):
Emitted by the orchestrator when the session ends. Campfire-key-signed.

Field layout:

session:close payload {
  session_id:  string   # campfire ID of this session campfire
  closed_at:   uint64   # unix nanosecond timestamp of closure
  reason:      string   # "expired" | "orchestrator-closed" | "eviction"
}

§1.5 Reserved-Op Floor LIST

Source: protocol-spec.md:1230–1242

The ten reserved ops form the reserved-op floor: a protocol-level minimum that no convention declaration and no parent grant can lower. The clamping order is fixed and one-directional: owner ceiling > parent grant > convention declaration. Convention authors are not in the trusted computing base for these operations.

Canonical list (10 ops):

disband | evict | admit | grant | revoke |
delegation-grant | delegation-revoke | delegation-accept |
member-roster | compaction

Architecture: the LIST lives at Layer 1 (cf-protocol/internal/reserved-ops.go); the ENFORCER (intercepts dispatch, consults the L1 list) lives at Layer 2; the EVALUATOR (applies actual grant-chain policy) lives at Layer 3 cf-authority. No convention may declare access to a reserved op without a grant from a principal with owner-level authority.

§1.6 future / fulfills Reserved Tags

Source: protocol-spec.md:1155–1159, 1214–1228

The "future" and "fulfills" tags are ratified as Layer-1 frozen reserved tags as of cf-protocol 1.0. They are member-signed (the sender's key covers them), not campfire-key-signed.

The DAG semantics are frozen together with the message envelope and cannot be split into a separate layer-3 convention. Antecedents []string sits at CBOR field 5 of the signed envelope. Closes OPEN-001.

§1.7 Antecedent/Fulfillment Fusion

Source: protocol-spec.md:1244–1250

Antecedent links and fulfillment semantics are inseparable from the signed-message substrate. The antecedents field is part of the signed message envelope (CBOR field 5 of the canonical layout, covered by the sender signature). The "future" and "fulfills" reserved tags and the Client.Await contract are Layer-1 primitives, not Layer-3 projections.

There is no cf-protocol-without-DAG profile, no cf-causality Layer-3 carve-out, and no internal sub-package boundary that promises future separability.

What this does NOT freeze: Activation semantics remain agent-local. The protocol defines what an antecedent and a fulfillment ARE — not what a consumer must DO when it sees one.

§1.8 Client.Await Contract

Source: protocol-spec.md:1252–1266

Client.Await is a Layer-1 read primitive. Its contract is wire-frozen at cf-protocol 1.0.

Fulfillment predicate. A message M fulfills future F if and only if:

Both conditions are required. A "fulfills"-tagged message that does not reference the target ID is NOT a fulfillment. A message that references the target ID without "fulfills" is a dependent, not a fulfillment.

Fulfillment ordering — earliest-timestamp wins. When multiple messages fulfill the same future, Client.Await returns the fulfillment with the earliest timestamp. Ties are broken by lexicographically smaller message ID. This ordering is deterministic: two implementations given the same set of fulfilling messages will always return the same winner.

Timeout. An Await with a specified timeout exits with ErrAwaitTimeout when the deadline expires. An Await with no timeout (zero value) blocks until fulfilled or context-cancelled. Negative timeout values are rejected at call time.


§2 — L3 cf-authority Freeze

Layer 3 surfaces frozen at cf-authority 1.0. Location: cf-conventions/cf-authority/trust/.

§2.1 Grant CBOR Layout

Source: cf-authority-spec.md:33–113

§2.1.1 Capability tuple (5-tuple + nonce)

Source: cf-authority-spec.md:34–44

Each capability is a CBOR map with integer keys:

Capability = {
  1: convention   ; tstr — exact convention name; no wildcards, no cross-convention
  2: op_pattern   ; tstr — within-convention op pattern; exact | alternation ("|") | "*"
  3: where        ; [+ WhereMatcher]  — OR-composition; empty = "wherever agent is a member"
  4: bounds       ; {tstr => BoundValue}  — reserved keys only; see §2.1.4
  5: until        ; int64 — mandatory expiry, nanoseconds-since-Unix-epoch UTC
  6: nonce        ; bstr — 16 bytes, random; grant-id = SHA-256(canonical-CBOR(Capability))
}

Field 5 (until) is mandatory. Grants without until MUST be rejected at parse time. There are no "forever" grants.

§2.1.2 Grant envelope (wire message payload)

Source: cf-authority-spec.md:51–65

A delegation:grant message carries a CBOR-encoded payload:

GrantPayload = {
  1: parent_grant_id   ; bstr | null — null for owner-root grants
  2: child_pubkey      ; bstr — Ed25519 public key, 32 bytes raw
  3: capabilities      ; [+ Capability]
  4: depth             ; uint — hop depth of this grant (0 = owner-root)
}

The grant-id is SHA-256(deterministic-CBOR(GrantPayload)). The grant message is signed by the granting party (the parent); the child's pubkey is embedded in the payload. Signature verification is the dispatcher's responsibility before invoking the evaluator.

§2.1.3 WhereMatcher

Source: cf-authority-spec.md:68–80

Three matcher kinds compose as OR. An empty where list means "wherever the agent is a member":

WhereMatcher =
  CampfireByID   ; {kind: 1, id: bstr}    — exact campfire hex-id
  | NamePrefix   ; {kind: 2, prefix: tstr} — campfire name prefix match
  | TagMatcher   ; {kind: 3, tag: tstr}    — campfire carries this tag

§2.1.4 BoundValue (reserved keys)

Source: cf-authority-spec.md:82–95

The bounds map uses reserved string keys only. Unknown keys MUST cause parse failure (fail closed):

KeyValue typeSemantics
rate{per: tstr, count: uint, window: tstr}Op rate cap
quota{unit: tstr, max: uint}Lifetime op count cap
spend{unit: tstr, max: uint}Monetary/scrip spend cap
ttluintSeconds; lifetime bound on any derived grant

§2.1.5 Depth limit and transitivity

Source: cf-authority-spec.md:98–113

Hard depth limit: 2. The deployment shape is human (depth 0) → agent (depth 1) → ephemeral worker (depth 2). Reserved ops are capped at depth 1.

Sub-grant scope is set-intersection on each axis: convention MUST equal parent's; op_pattern MUST be a within-convention subset; where union MUST be a containment-subset; bounds are MIN(parent, child) per axis; until ≤ parent.until. A grant that violates any of these is a scope-widening attempt → Deny / DenyReasonScopeWidening.

§2.2 Predicate AST Schema

Source: cf-authority-spec.md:119–231

§2.2.1 Leaf predicates (6 leaf kinds)

Source: cf-authority-spec.md:123–197

LeafPredicate =
  LevelPredicate          ; { kind: "level", n: uint }
  | GrantPredicate        ; { kind: "grant", convention: tstr, op: tstr }
  | GrantInPredicate      ; { kind: "grant_in", convention: tstr, op_glob: tstr, where: WhereMatcher }
  | GrantQuotaPredicate   ; { kind: "grant_quota", axis: tstr, bound: uint }
  | ChainToPredicate      ; { kind: "chain_to", pubkey: bstr }
  | ChainToQuorumPredicate ; { kind: "chain_to_quorum", m: uint, pubkeys: [+ bstr] }

There is no not: predicate — complement predicates are escalation primitives; banning them is a deliberate safety property of the language.

chain_to is by KEY (32-byte Ed25519 pubkey), not by name. Naming-as-authority is a security vector.

§2.2.2 Composite predicates

Source: cf-authority-spec.md:199–215

AllOfPredicate = { kind: "all_of", children: [+ PredicateAST] }
AnyOfPredicate = { kind: "any_of", children: [+ PredicateAST] }

Maximum nesting depth: 3. Predicates with depth > 3 MUST be rejected at parse time.

§2.2.3 PredicateAST (complete type)

Source: cf-authority-spec.md:217–231

type PredicateAST struct {
    Kind     PredicateKind
    Leaf     *LeafPredicate    // populated for leaf kinds
    Children []PredicateAST    // populated for all_of / any_of
}

Location: cf-conventions/cf-authority/trust/evaluator.go

§2.3 GateEvaluator Interface

Source: cf-authority-spec.md:239–327

// Frozen at cf-authority 1.0.
type GateEvaluator interface {
    Evaluate(ctx context.Context, req EvaluateRequest) EvaluateResult
}

EvaluateRequest carries: Request OpRequest, ChainMessages []message.Message, RootPrincipal ed25519.PublicKey, CurrentTime time.Time, RevocationView []RevocationViewEntry, OwnerPolicy OwnerPolicy. The evaluator assumes pre-verified inputs — signature verification is the caller's responsibility.

EvaluateResult carries: Decision Decision (Allow=1, Deny=2, Unresolvable=3), Reason DenyReason (set when Deny), MissingMessageID string (set when Unresolvable).

Location: cf-conventions/cf-authority/trust/evaluator.go

§2.4 DenyReason Enum (10 codes)

Source: cf-authority-spec.md:330–349

Stable string codes, frozen at cf-authority 1.0. Part of the wire freeze: two implementations that agree on Deny MUST agree on the reason code for any canonical input.

const (
    DenyExpired         DenyReason = "expired"              // grant.until < CurrentTime
    DenyRevoked         DenyReason = "revoked"              // active revocation in view
    DenyDepthExceeded   DenyReason = "depth_exceeded"       // chain depth > 2
    DenyScopeMismatch   DenyReason = "scope_mismatch"       // grant scope does not cover request
    DenyScopeWidening   DenyReason = "scope_widening"       // child claimed wider than parent
    DenyStaleRevocation DenyReason = "stale_revocation"     // view stale beyond max staleness
    DenyReservedOpFloor DenyReason = "reserved_op_floor"    // reserved-op depth/level floor
    DenyOwnerCeiling    DenyReason = "owner_ceiling"        // owner policy blanket denies
    DenyPredicate       DenyReason = "predicate_unsatisfied" // gate predicate not satisfied
    DenyStoreRead       DenyReason = "store_read_error"     // chain truncated; fail-closed
)

§3 — CBOR Field Tag Table

All CBOR in this spec is deterministic per RFC 8949 §4.2.1: definite-length encoding, map keys sorted by RFC 8949 §4.2.1 rules, smallest-int encoding, no NaN/Inf floats.

§3.1 L1 Message Envelope CBOR Fields

The signed envelope field positions are implicit in the Go struct and enforced by deterministic CBOR encoding. The antecedents field binds at position 5 in the canonical CBOR encoding — this is the key constraint motivating antecedent/fulfillment fusion at L1.

FieldWire typeVerifiedNotes
idUUID stringyesCovered by sender signature
senderbstr (32 bytes)yesEd25519 pubkey; must match signing key
payloadbstrTAINTEDSender-controlled content
tags[tstr]TAINTEDcampfire:* verified separately by campfire key
antecedents[tstr] (UUIDs)TAINTEDCBOR field 5; covered by sender signature
timestampuint64TAINTEDNanoseconds since Unix epoch
signaturebstr (64 bytes)yesEd25519 sig over (id+payload+tags+antecedents+timestamp)
provenance[ProvenanceHop]yesEach hop independently signed by campfire key

§3.2 L1 ProvenanceHop CBOR Fields

FieldWire typeVerifiedNotes
campfire_idbstr (32 bytes)yesEd25519 pubkey; must match hop signing key
membership_hashbstryesMerkle root
member_countuintyesDerivable from membership hash
join_protocoltstryesCampfire-asserted
reception_requirements[tstr]yesCampfire-asserted
timestampuint64yesCampfire-asserted; not sender-controlled
roletstryesOmitted when empty (backward compat); "blind-relay" for bridge hops
signaturebstr (64 bytes)yesEd25519 sig over (message.id + all fields above)

§3.3 L3 Capability CBOR Field IDs

Source: cf-authority-spec.md:37–44

Field IDField nameCBOR type
1conventiontstr
2op_patterntstr
3where[+ WhereMatcher]
4bounds{tstr => BoundValue}
5untilint64 (nanoseconds-since-Unix-epoch UTC)
6noncebstr (16 bytes)

§3.4 L3 GrantPayload CBOR Field IDs

Source: cf-authority-spec.md:55–61

Field IDField nameCBOR type
1parent_grant_idbstr | null
2child_pubkeybstr (32 bytes)
3capabilities[+ Capability]
4depthuint

§3.5 L3 WhereMatcher CBOR kind values

Source: cf-authority-spec.md:70–79

kind valueMatcher typePrimary field
1CampfireByIDid: bstr
2NamePrefixprefix: tstr
3TagMatchertag: tstr

§4 — Conformance Contract

§4.1 What IS frozen (cannot change in 0.30.x patch releases)

The following surfaces are frozen. A PR that modifies any of them after the 0.30 tag MUST include a major-version bump declaration (cf-protocol → 2.0 or cf-authority → 2.0, as appropriate) and a migration guide:

L1 cf-protocol 1.0 frozen surfaces:

L3 cf-authority 1.0 frozen surfaces:

§4.2 What is NOT frozen (may evolve in 0.30.x patch releases)

§4.3 Minor-compat CI workflow

Source: cf-authority-spec.md:527–530

Gate 4 (wire-format freeze): this snapshot is published; COMPATIBILITY.md shipped; minor-compat CI workflow active. No PR may modify frozen surfaces after the 0.30 tag without a major-bump declaration.

The conformance harness (cf-authority/trust/conformance/) runs twelve canonical test cases. Third-party implementations of GateEvaluator MUST pass all twelve before being eligible to back a cf-authority-conforming dispatcher.

Source: cf-authority-spec.md:439–520 (§5 Conformance Harness)


Snapshot generated: 2026-04-28. Status: FROZEN at cf 0.30 / cf-protocol 1.0 / cf-authority 1.0.
This document is the Phase 8 Gate 4 artifact. Tests: test/demo/20-wire-format-freeze.sh.

Source on GitHub →