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:
- Member key — signs the message envelope. The
signaturefield inMessageis the sender's Ed25519 signature over(id + payload + tags + antecedents + timestamp). - Campfire key — signs provenance hops and system events. The
signaturefield inProvenanceHopis the campfire's Ed25519 signature over(message.id + all hop fields including role).
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)
| Tag | Signed by | Introduced |
|---|---|---|
campfire:member-joined | campfire key | P0 |
campfire:member-evicted | campfire key | P0 |
campfire:member-left | campfire key | P0 |
campfire:eviction | campfire key | P0 |
campfire:rekey | campfire key (OLD key) | P0 |
campfire:disband | campfire key | P0 |
campfire:invite | member key (exception) | P0 |
campfire:vouch | member key (exception) | P0 |
campfire:revoke | member key (exception) | P0 |
campfire:compact | campfire key (full role required) | P1 |
campfire:view | campfire key (full role required) | P1 |
campfire:member-role-changed | campfire key | P1 |
0.30 additions (frozen at cf-protocol 1.0)
Source: protocol-spec.md:1144–1146, 1162–1212
| Tag | Signed by | Introduced |
|---|---|---|
campfire:visibility-changed | campfire key (NOT a member-signed exception) | 0.30 |
session:open | campfire key (NOT a member-signed exception) | 0.30 |
session:close | campfire 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.
future— a message taggedfutureis a commitment pending fulfillment. Its payload explains what qualifies as fulfillment.fulfills— a message taggedfulfillswith the future's ID inantecedentssatisfies the future. Both conditions are required (see Client.Await Contract, §1.8).
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:
- M carries the
"fulfills"tag, AND - F's message ID appears in M's
antecedentsarray.
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):
| Key | Value type | Semantics |
|---|---|---|
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 |
ttl | uint | Seconds; 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.
| Field | Wire type | Verified | Notes |
|---|---|---|---|
id | UUID string | yes | Covered by sender signature |
sender | bstr (32 bytes) | yes | Ed25519 pubkey; must match signing key |
payload | bstr | TAINTED | Sender-controlled content |
tags | [tstr] | TAINTED | campfire:* verified separately by campfire key |
antecedents | [tstr] (UUIDs) | TAINTED | CBOR field 5; covered by sender signature |
timestamp | uint64 | TAINTED | Nanoseconds since Unix epoch |
signature | bstr (64 bytes) | yes | Ed25519 sig over (id+payload+tags+antecedents+timestamp) |
provenance | [ProvenanceHop] | yes | Each hop independently signed by campfire key |
§3.2 L1 ProvenanceHop CBOR Fields
| Field | Wire type | Verified | Notes |
|---|---|---|---|
campfire_id | bstr (32 bytes) | yes | Ed25519 pubkey; must match hop signing key |
membership_hash | bstr | yes | Merkle root |
member_count | uint | yes | Derivable from membership hash |
join_protocol | tstr | yes | Campfire-asserted |
reception_requirements | [tstr] | yes | Campfire-asserted |
timestamp | uint64 | yes | Campfire-asserted; not sender-controlled |
role | tstr | yes | Omitted when empty (backward compat); "blind-relay" for bridge hops |
signature | bstr (64 bytes) | yes | Ed25519 sig over (message.id + all fields above) |
§3.3 L3 Capability CBOR Field IDs
Source: cf-authority-spec.md:37–44
| Field ID | Field name | CBOR type |
|---|---|---|
| 1 | convention | tstr |
| 2 | op_pattern | tstr |
| 3 | where | [+ WhereMatcher] |
| 4 | bounds | {tstr => BoundValue} |
| 5 | until | int64 (nanoseconds-since-Unix-epoch UTC) |
| 6 | nonce | bstr (16 bytes) |
§3.4 L3 GrantPayload CBOR Field IDs
Source: cf-authority-spec.md:55–61
| Field ID | Field name | CBOR type |
|---|---|---|
| 1 | parent_grant_id | bstr | null |
| 2 | child_pubkey | bstr (32 bytes) |
| 3 | capabilities | [+ Capability] |
| 4 | depth | uint |
§3.5 L3 WhereMatcher CBOR kind values
Source: cf-authority-spec.md:70–79
| kind value | Matcher type | Primary field |
|---|---|---|
| 1 | CampfireByID | id: bstr |
| 2 | NamePrefix | prefix: tstr |
| 3 | TagMatcher | tag: 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:
- Message envelope structure (fields, types, CBOR positions)
- Signature inputs: the set of fields covered by the sender's Ed25519 signature
- ProvenanceHop structure (fields, types, CBOR positions)
- Hop signature inputs:
message.id + all ProvenanceHop fields - Reserved tag namespace (
campfire:*) and signing rules (campfire-key vs. member-key) - The three member-signed exceptions:
campfire:vouch,campfire:revoke,campfire:invite campfire:visibility-changedfield layout and campfire-key-signing requirementsession:openandsession:closefield layouts and campfire-key-signing requirement"future"and"fulfills"tags as L1 reserved tags (member-signed; DAG semantics)antecedentsas CBOR field 5 of the signed envelope- Client.Await fulfillment predicate (fulfills tag + target-in-antecedents)
- Client.Await ordering rule (earliest-timestamp wins; lexicographic-message-ID tiebreaker)
- Reserved-op floor LIST (the canonical 10 ops:
disband | evict | admit | grant | revoke | delegation-grant | delegation-revoke | delegation-accept | member-roster | compaction) - Reserved-op clamping order: owner ceiling > parent grant > convention declaration
L3 cf-authority 1.0 frozen surfaces:
- Capability 5-tuple field IDs (CBOR integer keys 1–6)
- GrantPayload field IDs (CBOR integer keys 1–4)
- WhereMatcher kind values (1 = CampfireByID, 2 = NamePrefix, 3 = TagMatcher)
- BoundValue reserved key names (
rate,quota,spend,ttl) - Hard depth limit: 2
- LeafPredicate kind strings (6 kinds:
level,grant,grant_in,grant_quota,chain_to,chain_to_quorum) - CompositePredicate kind strings (
all_of,any_of) - Absence of
not:predicate (deliberate safety property) - Maximum nesting depth: 3
- GateEvaluator interface signature (Go types)
- DenyReason string codes (10 codes, as enumerated in §2.4)
- Determinism requirement: same inputs → same output, byte-for-byte
- Fail-closed requirement: Unresolvable on unresolvable chain (never Allow)
- Predicate-AST canonicalization rules (sort order for
all_of/any_ofchildren) - CBOR canonicalization rules (RFC 8949 §4.2.1 deterministic encoding)
§4.2 What is NOT frozen (may evolve in 0.30.x patch releases)
- Activation semantics: agent-local interpretation of antecedents and futures
- Convention declarations: new declarations may be added; existing declarations may be versioned
- Named-filter predicate grammar extensions (new leaf types) — subject to minor-version bump
- Transport implementations (filesystem, HTTP, GitHub) — not part of the wire format
- CLI/MCP surface: convenience sugar; not part of the wire format
- Filter optimization algorithms
- Retention policies (implementation hints)
§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.