Skip to content

Latest commit

 

History

History
1327 lines (929 loc) · 98.4 KB

File metadata and controls

1327 lines (929 loc) · 98.4 KB

Campfire Protocol Specification

Version: Draft v0.4 (0.19 protocol spec — superseded by 0.30 cf-protocol) Date: 2026-04-01 Author: Third Division Labs

Note: This document describes the 0.19 protocol surface. The 0.30 redesign replaces several concepts listed here. For the authoritative 0.30 architecture, see docs/ for cf-authority-spec.md, cf-discovery-spec.md, convention-sdk.md, and the design corpus in campfire-agent/docs/design/0.30-design.md. Key removals in 0.30: shared-key session tokens, present_as config field, recenter/walk_up center-finding primitives, GitHub transport. Key additions: cf-authority L3 evaluator, cf-identity, cf-session (per-participant ephemeral keys with scoped grants), cf-convention-extension, cf-discovery (beacon + naming + rate-limit declarations).

Changes from v0.3:

  • ProvenanceHop gains role field (CBOR key 8, omitempty, signed). Records the membership role of the relaying node at time of relay.
  • New membership role: blind-relay — forwards messages without reading encrypted content. Bridge transports annotate forwarded hops with this role.
  • EffectiveRole mapping updated to include blind-relay.

Overview

Campfire is a coordination protocol for autonomous agents. Agents communicate through campfires: groups with self-optimizing filters, enforceable reception requirements, and recursive composition. A campfire can be a member of another campfire. The protocol specifies message format, identity, membership semantics, filtering, and eviction. Transport is negotiable per campfire.

Concept Map

How the protocol's building blocks fit together.

Identity (Ed25519 keypair)
    │
    ├── is a Member of ──► Campfire ◄── is also a Member of ── another Campfire
    │                          │
    │                          ├── has Transport (filesystem / P2P HTTP / GitHub)
    │                          ├── has Join Protocol (open / invite-only)
    │                          ├── publishes Beacon (tainted advertisement)
    │                          └── contains Messages
    │
    └── signs ──────────────► Message
                                  │
                                  ├── Sender (verified — public key)
                                  ├── Payload (tainted)
                                  ├── Tags (tainted)
                                  ├── Antecedents (tainted — DAG edges)
                                  ├── Signature (verified)
                                  └── Provenance (verified — chain of hops)

Derivation chain:

  • A session (cf-session, L3) is an ephemeral-identity convention. Each participant gets their own Ed25519 keypair with a scoped grant from the session creator — per-participant attribution is preserved. The 0.19 model (shared ephemeral signing key embedded in a bearer token) is removed in 0.30.
  • A future is a message tagged "future" — a commitment waiting for a fulfillment (a message tagged "fulfills" with the future's ID in antecedents).
  • A convention is a typed operation declaration layered on top of send/read — not a protocol primitive.
  • A beacon is a signed advertisement. The campfire ID and signature are verified. Everything else (transport, description, join policy) is tainted.
  • Naming is a convention-message registry. Names are stored as messages in a campfire, resolved by direct-read — no RPC.
  • Provenance records bridging hops. Each hop is independently signed by the relaying campfire. The chain proves the message's path through the network.

Verified vs. tainted (summary):

Verified Tainted
Sender public key Payload
Signature Tags
Provenance hops Antecedents
Campfire ID (in beacon) Beacon description, transport, join policy
Membership hash Timestamp

Design Principles

  1. One interface. Campfire↔Member. A campfire doesn't know or care if its member is an agent or another campfire.
  2. One communication primitive. There are no DMs. A private conversation is a campfire with two members. All communication flows through campfires.
  3. Discovery through beacons and provenance. Campfires advertise through beacons (passive discovery). Once connected, further discovery happens through provenance chains (organic growth). No global registry.
  4. Receive is enforceable, send is not. A campfire can require members to accept certain message categories. A campfire cannot know what a member chose not to say.
  5. Filters are local, self-optimizing. Each edge has a filter on each end. Filters learn from outcomes. The protocol defines the filter interface, not the filter implementation.
  6. Transport is negotiable. The protocol defines the message envelope and semantics. How bytes move is agreed upon at join time, per campfire.
  7. Identity is cryptographic. No central authority. Your public key is your identity.
  8. Verified vs tainted. Every field in the protocol is either cryptographically verified or sender-asserted. Verified fields cannot be manipulated by any party. Tainted fields can contain anything, including adversarial content. Never make a trust decision based solely on tainted fields. Beacons are entirely tainted except for the campfire's identity and signature — they are advertisements, not facts.

Input Classification

Every field in every protocol structure is classified as verified or tainted. This classification governs how agents, filters, and implementations may use the field.

Verified. The field's value is derived from cryptographic operations or is independently verifiable. No single party can manipulate it. Examples: public keys (self-authenticating), signatures (cryptographically checkable), membership hashes (Merkle-verifiable), provenance chains (each hop independently signed). Verified fields are safe for trust decisions, access control, and filtering.

Tainted. The field's value is asserted by a party whose honesty is not guaranteed. The signature proves who asserted it, not whether it's true. Examples: message payloads, message tags, beacon descriptions, timestamps. Tainted fields are useful for signal, routing, and display — but MUST NOT be the sole basis for trust decisions, access control, or automated action.

The same field can change classification across contexts. A beacon's join_protocol is tainted (the campfire owner claims "I'm open" — they could lie). After joining, the campfire's observed join behavior is verified (you can see it enforcing or not enforcing). Pre-join fields are claims. Post-join fields are observations.

Field Classification by Structure

Message:

Field Classification Rationale
id verified Covered by sender's signature, unique by construction
sender verified Must match signature verification key
payload TAINTED Sender-controlled content
tags TAINTED Sender-chosen labels (except campfire:* which are verified against campfire key)
antecedents TAINTED Sender-asserted causal claims — "claims, not proofs"
timestamp TAINTED Sender's wall clock, not authoritative
signature verified Cryptographic proof of authorship
provenance verified Each hop independently signed and verifiable

ProvenanceHop:

Field Classification Rationale
campfire_id verified Must match hop signature verification key
membership_hash verified Merkle root, independently resolvable
member_count verified Derivable from membership hash
join_protocol verified Campfire-asserted (not sender-controlled)
reception_requirements verified Campfire-asserted (not sender-controlled)
timestamp verified Campfire-asserted (not sender-controlled; accuracy not guaranteed, but sender cannot manipulate it)
signature verified Campfire signs all fields above

Beacon:

Field Classification Rationale
campfire_id verified Public key, must match signature
join_protocol TAINTED Owner-asserted policy claim — could lie
reception_requirements TAINTED Owner-asserted policy claim
transport TAINTED Owner-asserted connection details — could point anywhere
description TAINTED Owner-asserted text — prompt injection vector
tags TAINTED Owner-asserted labels
signature verified Proves campfire owner authored all fields above

A beacon is an advertisement. The only verified facts are who is advertising (campfire_id) and that they authored it (signature). Everything they say about themselves — their policies, their purpose, how to connect — is a claim. Agents SHOULD evaluate trust (shared campfire memberships, vouch history, known keys) before acting on tainted beacon fields.

Implications for Agents

  1. Discovery is not trust. Discovering a beacon means you found an advertisement. It does not mean the campfire is safe to join, that the description is honest, or that the transport endpoint is benign.
  2. Filter on verified fields first. When deciding whether to process a message or join a campfire, apply verified-field conditions (sender key, trust level, provenance depth) before reading tainted fields.
  3. Tainted content is a prompt injection vector. Beacon descriptions, message payloads, and message tags are arbitrary strings from potentially adversarial parties. Agents that feed these strings into LLM prompts or decision logic without sanitization are vulnerable.
  4. Content graduation applies to beacons too. Just as messages from low-trust senders can be reduced to metadata-only (see Content Access Graduation), beacon tainted fields should be withheld until the agent has evaluated the campfire's trust posture.

Primitives

Identity

Every participant (agent or campfire) holds a keypair.

Identity {
  public_key: bytes     # Ed25519 public key
}

The public key is the permanent, verifiable identity. An agent has no standalone address. Agents are reachable through their campfire memberships. Transport is always a campfire concern.

Message

Every message in the protocol shares the same envelope.

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
}

Tags are freeform strings. The protocol doesn't define a tag vocabulary beyond the reserved namespace below. Campfires define which tags are reception-required. Filters operate on tags. Agents apply tags when sending. Examples: schema-change, breaking-change, status-update, file-modified:src/main.rs.

Reserved Tag Namespace

Tags in the campfire: namespace are reserved for protocol operations. By default, messages tagged with any campfire:* tag MUST be signed by the campfire's own key. Receivers MUST reject any message carrying a campfire:* tag whose signature does not verify against the campfire's public key. This is enforced cryptographically: the receiver checks the signature field against the campfire's known public key, not against the sender field.

Exceptions. The following campfire:* tags are member-signed: campfire:vouch, campfire:revoke (see Trust), and campfire:invite (see Membership). Receivers accept these tags when the signature verifies against any current member's public key. All other campfire:* tags require the campfire's signature.

Ordering

The protocol does not guarantee delivery order. Antecedents provide causal ordering where it matters. Wall clock timestamps are informational, not authoritative. A distributed system cannot guarantee total order across independent senders — the message DAG makes this unnecessary.

Antecedents

Antecedents are message IDs that this message builds on, replies to, or depends on. They form a directed acyclic graph (DAG) of causal relationships between messages. See Message DAG below.

Provenance Hop

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 records the membership role of the relaying node at the time of relay (e.g. "full", "writer", "observer", "blind-relay"). It is included in the signed input. When empty, the field is omitted from the CBOR encoding so that hops produced by pre-role implementations verify identically.

A bridge relay sets role to "blind-relay" on forwarded messages regardless of the bridge's actual membership role. This marks the hop as a blind relay for provenance tier computation (see Membership Roles below). The role annotation is per-hop, not a membership change — the bridge's membership role is unchanged.

For the bridge transport relay model (re-publish vs pass-through modes, non-goals, and threat model), see cf-protocol/docs/bridge.md.

The membership hash allows verification without embedding the full member list. Any member can request the full list from the campfire to resolve the hash.

A message originating in a deeply nested campfire and reaching an agent five levels up carries five hops. Each hop is independently verifiable. The full chain proves the path the message traveled and the state of each campfire at the time of relay.

Campfire

Campfire {
  identity: Identity
  members: [Member]
  join_protocol: JoinProtocol
  reception_requirements: [string]   # tags all members must accept
  threshold: uint                    # minimum signers for provenance hops (1 = any single member)
  filters: [MemberFilter]           # per-member, bidirectional
  transport: TransportConfig         # how this campfire moves bytes
  created_at: uint64
}

The threshold field determines how many members must cooperate to sign a provenance hop on behalf of the campfire. A threshold of 1 means any single member holding the campfire's key can sign alone (equivalent to a shared key). A threshold equal to the member count means unanimous agreement. The campfire's public key is the same regardless of threshold — verification is identical. Only the signing ceremony differs. See Threshold Signatures below.

Member

Member {
  identity: Identity
  joined_at: uint64
  filter_in: Filter      # campfire's filter on what it sends to this member
  filter_out: Filter     # campfire's filter on what it accepts from this member
}

A member's own inbound filter (what the member chooses to process) is local to the member and not visible to the campfire. The campfire only controls its side of each edge.

Filter

Filter {
  rules: [FilterRule]
  pass_through: [string]    # tags that always pass (reception requirements are automatically here)
  suppress: [string]        # tags that never pass
  # everything else: evaluated by rules
}

FilterRule {
  condition: expression     # implementation-defined, operates on message metadata
  action: "pass" | "suppress"
  confidence: float         # how confident the filter is in this rule, from optimization
}

Filters self-optimize by observing outcomes. The protocol defines the interface (tags, pass/suppress, rules with confidence) but not the optimization algorithm. A simple implementation might track which suppressed messages correlated with rework. A sophisticated one might use the full message history.

Filter Input Classification

Filter inputs are classified as verified or tainted per the Input Classification section above. Filters operate on both — but conditions that gate trust decisions (content graduation, access control, eviction triggers) MUST include at least one verified dimension. A filter that uses only tainted inputs (e.g., "suppress messages tagged X") is a noise filter, not a security boundary.

Filter conditions compose with AND across input dimensions. A filter that specifies both a tag set and a trust threshold requires both conditions to be satisfied.

Filters express accepted tag sets. A member's filter declares which tags it will accept. Messages carrying tags not in the declared set are dropped before reaching the member. Reception requirements override this: required tags are always in the accepted set regardless of the member's filter declaration.

Content Access Graduation

Trust level is a filterable dimension. A member MAY declare a trust threshold on their inbound filter. Messages are then delivered in two tiers based on the sender's trust level in the campfire:

Below threshold. The filter passes metadata only: sender key, tags, timestamp, and message byte length. The payload is withheld. The member sees that a message exists, who sent it, what it's about, and how large it is — but not its content.

At or above threshold. Full message content passes. No metadata-only reduction.

A member MAY explicitly request withheld content through a pull operation. This is the taint-crossing boundary: the member consciously chooses to access content from a sender whose trust level does not meet their threshold. The protocol defines the boundary; the pull mechanism is transport-dependent.

Content access graduation is local to each member's filter. The campfire does not enforce it — the campfire delivers full messages. The member's local filter performs the reduction. This preserves the principle that filters are local.

Filter Transparency

Filters SHOULD provide aggregate pass/suppress statistics to the affected member (e.g., "N of your last M messages tagged X were suppressed"). Kerckhoffs's principle applies: filter effectiveness must not depend on rule secrecy, since filters built on protocol-derived inputs are robust to sender knowledge. Filter internals — rules, confidence scores, optimization state — remain opaque. Only aggregate delivery outcomes are visible to the member.

Reception requirements are automatically added to every member's pass_through list. A member whose local filter blocks a pass-through tag is in violation and subject to eviction.

Beacon

A beacon advertises a campfire's existence to potential members who have no existing connection.

Beacon {
  campfire_id: public_key              # [verified] the campfire's identity
  join_protocol: string                # [TAINTED] owner-asserted policy claim
  reception_requirements: [string]     # [TAINTED] owner-asserted policy claim
  transport: TransportConfig           # [TAINTED] owner-asserted connection details
  description: string                  # [TAINTED] owner-asserted purpose — prompt injection vector
  tags: [string]                       # [TAINTED] owner-asserted labels
  signature: bytes                     # [verified] campfire signs all fields above
}

A beacon contains enough information for an agent to decide whether to join and how to connect. It does not contain membership details, filter state, or message history.

The protocol defines the beacon data structure. Where and how beacons are published is a deployment concern. Beacon channels include but are not limited to:

  • Filesystem. A well-known directory (e.g. ~/.campfire/beacons/). Agents on the same machine discover each other by listing the directory.
  • Git repository. A .campfire/beacons/ directory in a repo. Clone or pull the repo, discover its campfires. Natural for development workflows.
  • DNS TXT records. _campfire._tcp.example.com. Internet scale, zero infrastructure beyond a domain.
  • HTTP well-known. example.com/.well-known/campfire. Standard web discovery pattern.
  • mDNS/Bonjour. Local network auto-discovery. Zero configuration.
  • QR code. Encode the beacon, print it, stick it somewhere. Physical bootstrap.
  • NFC tag. Tap to discover. Conference badges, device labels, room placards.
  • Paste. Copy the beacon into a chat, an email, a document. Works everywhere humans already communicate.
  • Bluetooth. Physical proximity discovery.

Some beacon channels are active (mDNS broadcasting continuously) and some are passive (a file sitting in a directory, a DNS record waiting to be queried). The campfire doesn't know how it was discovered. The agent doesn't need to know how the beacon was published. The beacon is self-contained and self-verifying (signed by the campfire's key).

Beacon channel moderation is a deployment concern, not a protocol concern. Open campfires on public channels accept the cost of evaluating join requests. Campfires that need protection use delegated or invite-only join protocols, where the delegate's filtering is the admission control. The join protocol is the defense against unwanted joins; each beacon channel is responsible for its own publication policies.

Message DAG

Messages form a directed acyclic graph through their antecedents field. While provenance chains track the routing path (which campfires relayed a message), antecedents track the causal path (what prompted this message). The two dimensions are orthogonal.

An antecedent is a message ID that this message builds on, replies to, or depends on. A message may reference zero or many antecedents. The antecedents field is always present (empty array when no references).

Antecedents are claims, not proofs. A message can reference an ID the recipient has never seen — the referenced message may live in another campfire, may not have been relayed yet, or may not yet exist. The protocol does not validate that referenced messages exist. Antecedents are covered by the sender's signature and cannot be tampered with in transit.

The DAG enables:

  • Thread structure. Replies reference the message they respond to. UIs can render conversations as trees.
  • Dependency chains. A message can declare "I build on X" where X hasn't been sent yet. When X arrives, dependents become actionable. See Futures and Fulfillment below.
  • Filter optimization. Filters can reason about causal relationships: suppressing a message that N other messages depend on has a computable cost.

Threshold Signatures

Provenance hops are signed "by the campfire." What this means depends on the campfire's threshold:

Threshold = 1 (shared key). Every member holds the campfire's full private key. Any single member can sign provenance hops. This is the simplest model — equivalent to the filesystem transport where the key sits in a shared directory. The tradeoff: a compromised member can forge provenance hops, and eviction requires generating a new keypair (see Eviction and Rekey below).

Threshold > 1 (threshold signatures). The campfire's private key is split among members using a threshold signature scheme (e.g., FROST for Ed25519). No single member holds the full key. M-of-N members must cooperate to produce a valid signature. The campfire's public key is the same — verification is identical to threshold = 1. Only the signing ceremony differs (multiple rounds of communication between signers).

Properties of threshold > 1:

  • A compromised member cannot forge provenance hops alone (they hold only a key share)
  • The campfire survives the loss of up to N - M members (the remaining M can still sign)
  • Signing a provenance hop requires a communication round between M members, adding latency proportional to threshold

Split prevention. Threshold > 1 makes campfire splits unambiguous. If a campfire of 5 members with threshold 3 splits into groups of 3 and 2, only the group of 3 can produce valid provenance hop signatures. The group of 2 cannot sign, cannot relay, and cannot claim the campfire's identity. The threshold is the quorum.

Threshold choice is a trust/latency tradeoff:

  • threshold = 1: fastest (single signer), weakest (any member can forge)
  • threshold = majority: balanced (requires cooperation, prevents minority forks)
  • threshold = N: slowest (unanimous), strongest (all members must agree on every relay)

The protocol does not mandate a specific threshold signature scheme. The requirements are: the scheme produces standard Ed25519 signatures verifiable with the campfire's public key, supports key re-sharing on membership changes, and does not require a trusted dealer after initial setup. FROST (Flexible Round-Optimized Schnorr Threshold signatures) satisfies these requirements.

Trust

Trust between members is established through vouches. A member vouches for another member's key by sending a signed message into the campfire:

  • campfire:vouch — a member asserts trust in a key. The payload identifies the key being vouched for. Signed by the vouching member's key.
  • campfire:revoke — a member withdraws a previous vouch for a key. Same structure as campfire:vouch.

Both are ordinary messages: they have IDs, travel through the campfire, appear in the DAG, and are covered by provenance chains. A campfire:revoke message references the original campfire:vouch in its antecedents.

Trust level is a derived metric, not a protocol field. The protocol does not define a trust score formula. It defines the message types (campfire:vouch, campfire:revoke) that carry the raw signal. Implementations derive trust level from the vouch history within a campfire.

Trust level is scoped to a campfire. A member's trust level in campfire A is independent of their trust level in campfire B. When a child campfire is a member of a parent, the child's trust level in the parent is determined by vouches from the parent's members — the child's internal vouch history is opaque to the parent.

Key rotation. An agent rotates keys by posting a campfire:vouch for their new key signed by their old key, then joining with the new key and leaving with the old. The vouch chain establishes continuity. No special protocol mechanism is needed — key rotation is a trust establishment operation using existing primitives.

Operations

Campfire Lifecycle

Create. Any agent can create a campfire. The creator generates a keypair for the campfire and becomes its first member. The creator specifies join protocol, reception requirements, threshold, and transport.

Retention. Each campfire declares its retention policy as part of its configuration. Members agree to the policy at join time. The protocol does not mandate a default retention period — implementations choose based on resource constraints.

Disband. The campfire sends a final message to all members (tagged campfire:disband) and stops accepting messages. Members are responsible for removing the campfire from their campfire list.

Membership

Join. An agent requests to join a campfire. The join protocol determines what happens:

  • open: agent is immediately admitted
  • invite-only: a current member must admit the agent
  • delegated: the campfire designates one or more members as admittance delegates

On join, the new member's transport details for this campfire are registered. The admitting member sends the new member campfire key material (the full private key for threshold = 1, or a new key share for threshold > 1), encrypted to the new member's public key. For threshold > 1, joining triggers a key re-sharing round to include the new member. The campfire sends a campfire:member-joined message to all existing members.

Admit. A current member sponsors a new member for admission. For invite-only, any current member can admit. For delegated, only designated admittance delegates can admit.

Admittance delegation. A campfire can delegate the admit/deny decision to any member. The delegate handles the interaction with the prospective member however it sees fit: verify a signed invitation, check a key signature chain, run a challenge, consult a reputation system, ask for payment, or anything else the two parties can mutually agree to navigate. The campfire doesn't know or care how the delegate decides. It honors the result.

A delegate is a regular member, subject to the same eviction rules as everyone else. A delegate that admits members who cause problems (spam, rework, noise) will be detected by the campfire's filter optimization and evicted. The campfire self-corrects for bad gatekeeping.

Since a member can be a campfire, a delegate can itself be a campfire of specialized verification agents. The admittance process can be as simple or as sophisticated as the campfire needs, without the protocol specifying any of it.

Invite. A member sends a campfire:invite message through an existing campfire to reach an agent they've discovered through a provenance chain. The invitation includes the target campfire's public key, transport config, and join protocol.

Invitations are ordinary messages. They travel through existing campfire infrastructure, subject to the same filters as any other broadcast. Invitation spam is handled by filters: a campfire whose members are tired of a member's invitation broadcasts will filter them, and persistent abuse triggers eviction through normal pattern detection.

Evict. A member is removed. Triggers:

  • Reception requirement violation: member's local filter is blocking a required tag (detected by failed delivery acknowledgment)
  • Pattern detection: member's silence is correlating with rework in other members (campfire's optimization loop detects this)
  • Manual: a member with eviction authority removes another member

Eviction authority is campfire-configurable via the threshold. For threshold = 1 campfires, the creator holds eviction authority. For threshold > 1, eviction requires a threshold signature — M-of-N members must cooperate to execute the eviction. Members can signal support for eviction through campfire:vouch/campfire:revoke trust primitives, but the eviction operation itself requires the campfire key.

The campfire sends a campfire:member-evicted message (tagged campfire:eviction) with the reason to all remaining members.

Eviction and Rekey

Eviction has cryptographic consequences because the evicted member holds key material.

Threshold = 1 campfires (shared key): The evicted member holds the full private key. The campfire must generate a new keypair and distribute the new key to remaining members. This changes the campfire's public identity.

Threshold > 1 campfires: The evicted member holds a key share, not the full key. The evicted member's share alone cannot produce valid signatures (the threshold prevents it), so split prevention is immediate — the evicted member cannot claim the campfire's identity. However, membership has changed, and the remaining members must establish new key shares. Two approaches:

  • Re-sharing (identity-preserving). Threshold schemes that support proactive re-sharing (e.g., Dynamic-FROST) can redistribute shares among the remaining members while preserving the campfire's public key. No identity change, no beacon updates, no parent campfire notification. This is the optimal path.

  • New DKG (rekey). If the threshold scheme does not support re-sharing, the remaining members run a new distributed key generation, producing a new keypair. The campfire's public identity changes, requiring a campfire:rekey message. The security properties (split prevention, threshold forgery resistance) hold in both cases — the rekey path is costlier but equally correct.

The same applies to join: a new member needs a key share, which requires either re-sharing or a new DKG.

In both cases, eviction sends a campfire:rekey system message (unless re-sharing preserved the public key):

When a campfire rekeys, it sends a campfire:rekey system message:

campfire:rekey {
  old_key: public_key       # the previous campfire identity
  new_key: public_key       # the new campfire identity
  reason: string            # "eviction", "key-rotation", etc.
  signature: bytes          # signed by the OLD key (proves continuity)
}

The rekey message is signed by the old key, proving that the holder of the old identity authorized the transition. Recipients (including parent campfires) verify the old signature and update their records to the new identity. The old key is considered revoked.

A fork that retains the old key cannot produce a valid campfire:rekey message pointing to the legitimate group's new key. The rekey message is the proof of succession.

Leave. A member voluntarily departs. The campfire sends a campfire:member-left message to remaining members.

Messaging

Broadcast. A member sends a message to a campfire. The campfire:

  1. Verifies the sender's signature
  2. Applies the sender's filter_out (campfire's filter on what it accepts from this member)
  3. If the message passes, appends a provenance hop and delivers to all other members
  4. For each recipient, applies their filter_in (campfire's filter on what it sends to this member)
  5. Delivers to recipients whose filters passed

Relay. When a campfire is a member of a parent campfire, broadcasts that pass the campfire's own outbound filter to the parent are relayed as broadcasts from the campfire (not from the original sender). The provenance chain preserves the original sender and all intermediate hops. The parent campfire sees the child campfire as the immediate sender and applies the child's filter_out accordingly.

Futures and Fulfillment

A future is a message tagged future. It describes work that is expected or needed — a review, a decision, a deliverable. Its payload explains what qualifies as fulfillment. A future is a real message: it has an ID, a sender, a signature, and it travels through the campfire like any other broadcast.

A fulfillment is a message tagged fulfills with the future's ID in its antecedents. It satisfies the expectation the future described.

A dependent is any message with a future's ID in its antecedents (without the fulfills tag). The sender is declaring: "this message builds on the outcome of that future." The dependent exists in the campfire immediately — it is signed, relayed, and stored. But agents treat it as pending until its future antecedent is fulfilled.

The protocol defines the DAG structure. Activation semantics are agent-local. The protocol does not enforce whether agents act on messages with unfulfilled antecedents. An agent may wait, act speculatively, or ignore antecedent state entirely. The graph is data; interpretation is local.

Example: Coordinating a Schema Migration

Agent A sends M1:
  tags: [future, schema-review]
  payload: "review migration v3 against schema constraints"
  antecedents: []

Agent A sends M2:
  tags: [migration]
  payload: "run migration v3"
  antecedents: [M1]          ← depends on the future

Agent A sends M3:
  tags: [deploy]
  payload: "deploy after migration"
  antecedents: [M2]          ← depends on the migration

Agent B sends M4:
  tags: [fulfills, schema-review]
  payload: "approved, one naming issue on line 42"
  antecedents: [M1]          ← fulfills the future

Agent A declared the entire execution plan as a message DAG before any work was done. The only gate was M1 — the schema review. Agent B fulfilled it by sending M4 referencing M1. Agents observing the campfire can now see: M1 is fulfilled (M4 references it with tag fulfills), M2's antecedent is resolved, M3's antecedent (M2) is resolved transitively.

No coordinator assigned the review. No central task system tracked the dependency. The messages themselves are the coordination mechanism. Five agents in a campfire can each see open futures and decide what to work on — the DAG makes the work visible.

Filter Implications

Futures give filters precise cost information. A filter considering whether to suppress schema-review messages can compute: "there are N open futures tagged schema-review with M dependent messages waiting. Suppressing this category has cost proportional to N × M." This is significantly more precise than inferring rework from behavioral correlation.

Await

An await is a blocking read for a specific fulfillment. An agent that has posted a future can await its fulfillment without abandoning its current session or context.

Semantics: Given a campfire ID and a message ID (the future), an await polls the campfire for any message that (a) carries the fulfills tag and (b) includes the target message ID in its antecedents. When such a message arrives, the await returns it and exits. If a timeout is specified and expires before fulfillment, the await exits with an error.

Why it matters. Without await, an agent that needs a decision must stop, lose its session context, and be re-dispatched after the decision arrives. Await preserves the agent's full context window — the agent blocks in-place and resumes exactly where it left off when the fulfillment appears. This collapses multi-dispatch orchestration loops into zero extra dispatches.

Activation semantics remain agent-local. Await is a convenience for agents that choose to block on a future. Agents may still poll manually, act speculatively, or ignore futures entirely. The protocol defines what fulfillment looks like in the DAG; await is a read pattern over that structure.

Escalation Tags

Futures used for escalation — where an agent needs a ruling from a peer, architect, or human — use the escalation tag alongside future to signal that the message requires external resolution. Subtypes are expressed as additional tags:

Tag Meaning
escalation This future requires external resolution
architecture Architecture or design decision needed
scope Scope or requirements clarification needed
interface Interface change approval needed
decision Response to an escalation (used with fulfills)

Example escalation flow:

Worker sends M1:
  tags: [future, escalation, architecture]
  payload: "optimistic vs pessimistic locking for session store?"

Architect sends M2:
  tags: [decision, fulfills]
  antecedents: [M1]
  payload: "optimistic — conflict rate is <1%, retry is cheaper than lock contention"

The worker awaits M1's fulfillment. When M2 arrives, the worker resumes with the architect's decision in hand, full context preserved.

Private Conversations

There is no DM primitive. A private conversation between two agents is a campfire with two members. To initiate a private conversation:

  1. Agent A sees Agent B's public key in a provenance chain
  2. Agent A creates a new campfire with the desired transport
  3. Agent A sends a campfire:invite message through a campfire that can reach Agent B (identified from the provenance chain)
  4. Agent B receives the invitation, inspects it, and joins if interested

The resulting two-member campfire has all the same properties as any other campfire: filters, reception requirements, provenance. The protocol doesn't special-case it.

CLI sugar. An implementation may provide a cf dm <public_key> "message" command that automates steps 1-4: creates a two-member campfire (or reuses an existing one), sends the invitation if needed, and delivers the message. This is convenience, not protocol. Under the hood it's campfire creation and a broadcast.

Reception Requirement Enforcement

A campfire tracks delivery acknowledgment per member per required tag. The protocol does not specify the acknowledgment mechanism (it's transport-dependent), but the semantics are:

  1. Campfire delivers a message tagged with a reception requirement to a member
  2. The transport layer provides a delivery acknowledgment (HTTP 200, TCP ACK, filesystem read confirmation, whatever the negotiated transport supports)
  3. If acknowledgment fails repeatedly (threshold is campfire-configurable), the campfire initiates eviction

"Repeatedly" is intentional. A single failed delivery might be a network issue. Consistent failure to acknowledge required messages is a filter violation.

Filter Optimization

Filters self-optimize over time. The protocol defines the inputs available for optimization, not the algorithm.

Available inputs:

  • Message history (what was sent, by whom, with what tags)
  • Message DAG (antecedent relationships, open futures, fulfillment chains)
  • Delivery acknowledgments (who received what)
  • Behavioral correlation: did a member's subsequent messages reference or respond to a broadcast? Did rework occur in members who didn't receive a broadcast?
  • Member activity patterns: frequency, tags used, campfire creation rate

Optimization target: minimize (total token cost of broadcasts delivered) + (rework cost from broadcasts suppressed). The specific cost function is campfire-configurable.

Constraints: reception requirements are hard constraints. The optimizer cannot suppress required tags regardless of cost.

Recursive Composition

A campfire can be a member of another campfire. The child campfire:

  • Has its own keypair (appears as a regular member to the parent)
  • Applies its own outbound filter before relaying to the parent
  • Receives broadcasts from the parent and applies its own inbound filter before relaying to its members
  • Is subject to the parent's reception requirements (and must ensure it can fulfill them)

The parent has no visibility into the child's internal structure. The child's membership list, internal messages, and filter state are opaque. The parent only sees: a member with a public key and a pattern of broadcasts and acknowledgments.

When a child campfire relays a message to the parent, the message's antecedents travel with it. The parent may not have the referenced messages — they may exist only within the child campfire. This is expected. Antecedents are informational claims, not resolvable pointers. The parent cannot use antecedents to peer into the child's internal message graph. Child campfire opacity is fully preserved.

A child campfire that joins a parent with reception requirement schema-change must ensure its own members produce schema-change messages when appropriate. The child can't enforce this on its members (send is not enforceable), but the child will be evicted from the parent if it fails to relay schema-change messages when they're relevant. The pressure propagates down without the parent needing to know about the child's members.

Conventions Layer

The Conventions Layer defines how agents express structured, repeatable operations on top of the campfire message protocol. A convention is a named, versioned set of operation declarations. Each declaration specifies the operation's argument schema, the tags it produces, its antecedent requirements, its signing mode, and optional rate limits. The Conventions Layer sits above the message envelope (§ Primitives) and below application-specific logic.

Declarations

A declaration is a JSON document that fully specifies one operation within a convention. Declarations are published to a campfire as ordinary messages tagged convention:operation. The payload MUST conform to the following schema:

{
  "convention":          "string (required) — convention name, e.g. \"campfire-social\"",
  "version":             "string (required) — semver or opaque string, e.g. \"0.1\"",
  "operation":           "string (required) — operation name, e.g. \"post\"",
  "description":         "string (optional) — human-readable summary",
  "supersedes":          "string (optional) — message ID of the declaration this replaces",
  "signing":             "string (required) — one of: member_key | campfire_key | convention_registry",
  "antecedents":         "string (optional) — antecedent rule (see below); default: none",
  "args": [
    {
      "name":        "string (required)",
      "type":        "string (required) — one of: string | integer | duration | boolean | key | campfire | message_id | json | tag_set | enum",
      "required":    "boolean (optional, default false)",
      "default":     "any (optional) — applied when argument is absent",
      "description": "string (optional)",
      "max_length":  "integer (optional) — string max byte length",
      "min":         "integer (optional) — integer lower bound (inclusive)",
      "max":         "integer (optional) — integer upper bound (inclusive)",
      "max_count":   "integer (optional) — maximum items when repeated=true",
      "pattern":     "string (optional) — safe regex anchored to full value",
      "values":      "[string] (optional) — allowed enum values",
      "repeated":    "boolean (optional, default false) — allow multiple values"
    }
  ],
  "produces_tags": [
    {
      "tag":         "string (required) — static tag or glob e.g. \"topic:*\"",
      "cardinality": "string (required) — one of: exactly_one | at_most_one | zero_to_many",
      "max":         "integer (optional) — max count for zero_to_many"
    }
  ],
  "rate_limit": {
    "max":    "integer (required) — maximum invocations per window",
    "per":    "string (required) — one of: sender | campfire_id | sender_and_campfire_id",
    "window": "string (required) — duration string, e.g. \"1m\", \"24h\""
  },
  "steps": [
    {
      "action":         "string (required) — one of: send | query",
      "description":    "string (optional)",
      "tags":           "[string] (optional) — tags for send step; may contain $binding.field references",
      "antecedents":    "[string] (optional) — antecedent refs for send step",
      "future_tags":    "[string] (optional) — tags for query step's future message",
      "future_payload": "object (optional) — payload for query step's future message",
      "result_binding": "string (optional) — binds query step result to this name"
    }
  ],
  "min_operator_level": "integer (optional, default 0) — minimum operator provenance level required",
  "views": [
    {
      "name":        "string (required) — view identifier within the campfire",
      "description": "string (optional)",
      "predicate":   "string (required) — S-expression predicate (see Named Views)"
    }
  ]
}

Field Classification

Declaration messages are ordinary campfire messages. The convention:operation tag is verified by receivers as a member-key-signed message (or campfire-key-signed for signing: campfire_key). The JSON payload is tainted — its fields carry the author's assertions about the operation schema. The executor enforces the declared schema when an operation is invoked; the declaration itself is not a security boundary.

Signing Modes

Value Who signs invocations When to use
member_key Any member with write access Standard operations open to all members
campfire_key Campfire key holder only Authority-bearing operations (e.g., publishing new declarations)
convention_registry Convention registry campfire Declarations promoted through a trusted registry

Operations with signing: campfire_key MUST be single-step declarations (no steps). Multi-step workflows MUST use member_key signing.

Antecedent Rules

The antecedents field specifies how the executor threads outgoing messages into the message DAG (see § Message DAG). Valid values:

Rule Behaviour
none (default) No antecedents set
exactly_one(target) Requires a message_id-typed arg; that arg's value becomes the sole antecedent
exactly_one(self_prior) Locates the caller's most recent message with the same operation tag; requires it to exist
zero_or_one(self_prior) Like exactly_one(self_prior) but allows genesis: if no prior message exists, sends with no antecedent

Arg Types

Type Description
string UTF-8 string; max_length, pattern apply
integer Signed integer; min, max apply
duration Duration string: "1m", "24h", "7d"
boolean JSON boolean
key 64-character hex-encoded Ed25519 public key
campfire Campfire ID (hex-encoded public key)
message_id UUID message identifier
json Arbitrary JSON string (must be valid JSON)
tag_set Array of tag strings
enum String constrained to values list

Rate Limits

When rate_limit is present, the executor enforces it before sending. The window MUST be at least "1m". The max MUST NOT exceed 100 (ceiling enforced by the executor; values above this are clamped). Rate limit state is scoped per the per field:

  • sender — per sender public key
  • campfire_id — per target campfire
  • sender_and_campfire_id — per (sender, campfire) pair

Rate limit state persists across multiple executor instances within the same process via a process-level singleton.

Tag Glob Rules

produces_tags entries with a tag ending in * are glob rules that map from argument values. For example, "topic:*" with cardinality zero_to_many collects all values of args whose name matches topic (or topics) and emits a topic:<value> tag for each. Exact (non-glob) tags are static — they appear unconditionally when cardinality is exactly_one, or conditionally otherwise.

Tag glob composition rules:

  • exactly_one — exactly one matching value MUST be present
  • at_most_one — zero or one matching values allowed
  • zero_to_many — any number of values; max limits the count when set

Tags in the campfire: namespace and naming: namespace MUST NOT appear in produces_tags (reserved prefix denylist), except for declarations from the convention-extension and naming-uri conventions respectively, which define those namespaces.

Executor

The Executor (pkg/convention.Executor) is the protocol component that validates, composes, and dispatches convention operations. It is backed by a protocol.Client and a sender public key.

Execution pipeline for a single-step operation:

  1. Provenance gate. If min_operator_level > 0, the executor queries the attached ProvenanceChecker for the sender's operator provenance level. If the sender's level is below the declared minimum, execution is rejected. See Operator Provenance Convention v0.1 §8.
  2. Arg validation. The executor validates the provided args map against the args descriptors: required fields must be present, undeclared fields are stripped (strict allow-listing), types are enforced, constraints applied (max_length, min, max, pattern, values, max_count).
  3. Default application. Missing optional args with declared defaults are filled in.
  4. Tag composition. The executor builds the outgoing tag list from produces_tags rules and the resolved args (see Tag Glob Rules above).
  5. Tag denylist check. Composed tags are checked against the reserved prefix and exact denylist. Any violation causes execution to fail.
  6. Antecedent resolution. The antecedent rule is applied (see Antecedent Rules above). For self_prior rules, the executor reads messages from the campfire to find the caller's prior message.
  7. Rate limit check. If rate_limit is declared, the executor checks the current window count before sending.
  8. Payload construction. The resolved args map is JSON-marshaled as the message payload.
  9. Send. The executor calls protocol.Client.Send with the composed tags, antecedents, and payload. The signing mode determines whether the campfire key or the member key is used.
  10. Rate limit record. After a successful send, the rate limit window counter is incremented.

Multi-step workflows replace steps 2–9 with a sequential step loop. Each step is either a send (emit a message with composed tags) or a query (send a future-tagged message and block until fulfilled; bind the fulfillment payload). Steps may reference prior step results via $binding.field variable substitution. The total workflow timeout is 120 seconds; each step timeout is 30 seconds.

Error semantics. Execution is atomic: if any pipeline step fails, no message is sent. The caller receives a descriptive error. Rate limit counters are only incremented on successful send.

Convention Registration

Declarations reach members through the campfire message stream. The registration lifecycle is:

  1. Publish. The convention author sends a message tagged convention:operation into a convention registry campfire — a campfire dedicated to holding declarations. The payload is the declaration JSON. For authority-bearing operations, the message MUST be signed with the campfire key (signing: campfire_key).

  2. Join imports declarations. When an agent joins a campfire that holds convention:operation messages, those messages are available in the message stream. Agents (or the Server SDK) read the stream, parse declarations via convention.Parse, and make them available for dispatch.

  3. Supersede. A newer declaration may reference the message ID of a prior declaration in its supersedes field. The executor and toolgen layers treat the prior declaration as superseded — it remains in the message store but is excluded from active operation dispatch.

  4. Revoke. A message tagged convention:revoke (sent by the campfire key holder) permanently removes a declaration from active use. Revoked declarations are retained in the message store for auditability.

Declaration authority. The trust level granted to a declaration depends on its signer (see SignerType):

  • campfire_key — declaration is campfire-key-authorized; full operational authority within that campfire
  • convention_registry — declaration arrived through a trusted convention registry campfire
  • member_key — declaration is member-asserted; lowest authority

The executor uses SignerType to determine what operations a declaration may legitimately declare (e.g., only campfire-key-signed declarations may declare signing: campfire_key operations).

The convention-extension convention. The infrastructure convention (convention-extension) provides the bootstrap primitives: promote, supersede, and revoke. These are the only declarations embedded in the binary. All other conventions arrive through the message stream. The promote operation itself is signed by the campfire key, ensuring only the campfire owner can publish new declarations to a registry.

Server SDK

The Server SDK (pkg/protocol.Client and pkg/convention.Executor) is the programmatic interface for agents and services implementing the campfire protocol.

Client (protocol.Client) handles the transport layer:

  • Init(cfHome) — generate or load Ed25519 identity, open SQLite store, return *Client
  • Send(SendRequest) — sign and deliver a message to a campfire
  • Read(ReadRequest) — retrieve messages with tag/sender/cursor filters; returns []protocol.Message
  • Get(id) — fetch a single message by exact ID from the local store
  • GetByPrefix(prefix) — fetch a single message by unambiguous ID prefix
  • Await(AwaitRequest) — block until a specific future is fulfilled
  • Subscribe(ctx, SubscribeRequest) — live message stream via poll loop
  • PublicKeyHex() — hex-encoded Ed25519 public key of the client's identity

Client is initialized with a local message store and an identity keypair. It is not safe for concurrent use — one Client per goroutine. Transport selection is automatic: the campfire's membership record determines whether to use filesystem, GitHub Issues, or P2P HTTP transport. Application code is transport-agnostic.

Transport abstraction. At create/join time, callers pass a typed transport config. The Transport interface is sealed to three concrete types in pkg/protocol:

  • FilesystemTransport{Dir} — local filesystem, sync-before-query
  • P2PHTTPTransport{Transport, MyEndpoint, PeerEndpoint, Dir} — peer-to-peer HTTP
  • GitHubTransport{Owner, Repo, Branch, Dir, Token} — GitHub Issues as message store

After joining, no transport config is needed in application code. The membership record carries the transport type and all routing information.

protocol.Message is the SDK-facing message type returned by all read operations:

type Message struct {
    ID          string
    CampfireID  string
    Sender      string   // hex-encoded Ed25519 public key
    Payload     []byte   // [TAINTED] sender-controlled
    Tags        []string // [TAINTED] sender-chosen labels
    Antecedents []string // [TAINTED] sender-asserted causal claims
    Timestamp   int64
    Instance    string   // [TAINTED] sender-asserted role label
    Signature   []byte
    Provenance  []message.ProvenanceHop // [verified]
}

Use msg.IsBridged() to test whether any provenance hop is a blind-relay (i.e., the message was bridged from an external system such as Teams or Slack).

Executor (convention.Executor) wraps Client with convention dispatch:

exec := convention.NewExecutor(client, client.PublicKeyHex())
// Optionally attach operator provenance enforcement:
exec = exec.WithProvenance(myProvenanceChecker)

Invoke an operation:

err = exec.Execute(ctx, decl, campfireID, map[string]any{
    "task_id": "task-001",
    "result":  "done",
})

Execute runs the full execution pipeline (validation → tag composition → rate limiting → send). The caller supplies the Declaration (loaded from the message stream or constructed in code), the campfire ID, and the arg values. The executor handles everything else.

Declaration construction. Declarations are either:

  • Parsed from convention:operation messages in the campfire stream via convention.Parse
  • Constructed directly in Go code as *convention.Declaration structs (for well-known protocols compiled into an agent)

Both forms are equivalent to the executor. Parsed declarations carry source metadata (MessageID, SignerKey, SignerType) that compiled declarations do not.

Tool Surface Generation

The same declarations that drive the executor also generate the CLI and MCP tool surfaces. This ensures CLI commands, MCP tools, and programmatic Execute calls are always in sync — there is one source of truth.

CLI tools (cf <campfire-id> <operation> [--arg value ...]): The CLI reads convention:operation messages from the target campfire's stream, parses them into Declaration structs, and dynamically registers a subcommand for each active operation. Arg descriptors become flags. The executor handles dispatch.

MCP tools (pkg/convention.GenerateTool): Each declaration is transformed into an MCP tool descriptor with a JSON Schema input schema derived from the args descriptors. Operation name becomes the tool name (with convention-namespace prefix on collision). Tool descriptions are truncated to 80 characters for MCP protocol compliance.

tool, err := convention.GenerateTool(decl, campfireID)
// tool.Name        — e.g. "post" or "social_post"
// tool.Description — truncated description
// tool.InputSchema — JSON Schema object

The MCP server (cmd/cf-mcp) discovers conventions by reading convention:operation messages from the campfires an agent belongs to, generates tools for each active declaration, and serves them via the MCP protocol. When a new declaration arrives (or an existing one is superseded or revoked), the tool surface updates without restarting the server.

View tools. Declarations with a views field generate additional read-only MCP tools alongside the write operations. Each view becomes a tool that materializes the named view predicate against the campfire's message store. See § Named Views for predicate semantics.

Normative constraint. Implementations MUST derive CLI and MCP tool surfaces from convention declarations. Hardcoded tool lists that diverge from the live declaration stream are a conformance violation — they would allow a campfire's available operations to drift from the tools that agents and users can invoke.

Transport Negotiation

Transport is specified per campfire at creation time and agreed upon by members at join time.

TransportConfig {
  protocol: string       # "filesystem", "p2p-http", "ws", etc.
  config: map            # protocol-specific configuration
}

A member that cannot speak the campfire's transport cannot join. Transport migration (campfire switches transport because requirements changed) requires agreement from all current members or re-creation of the campfire.

SDK representation. The Go SDK exposes TransportConfig as a sealed Transport interface with three concrete types: FilesystemTransport, P2PHTTPTransport, and GitHubTransport. These are passed to CreateRequest.Transport and JoinRequest.Transport. After joining, application code is transport-agnostic — the campfire's membership record carries the transport configuration and the Client routes automatically. See the SDK reference for details.

The protocol is transport-agnostic. The only requirement is that the transport supports:

  • Reliable delivery (or at least delivery acknowledgment)
  • Sender authentication (the transport must not allow spoofed sender identity)

Transport Models

Filesystem. Members share a directory. Messages are files. The campfire's key material sits in the directory. Suitable for agents on the same machine. Threshold = 1 (implicit — filesystem access grants full key access).

Peer-to-peer HTTP. Members communicate directly with each other over HTTP. No relay. No central server. Each member runs a small HTTP handler as part of their agent process:

POST /campfire/{id}/deliver      — receive a message from a peer
GET  /campfire/{id}/sync         — serve messages since a timestamp (catch-up)
POST /campfire/{id}/membership   — receive membership change notifications

When a member sends a message, they fan out to all other members' endpoints. If a member is unreachable, other members who received the message gossip it forward — a member that receives a message checks whether all peers have acknowledged it and forwards to those who haven't. Messages propagate through the mesh as long as any path between members exists.

Members behind NAT that cannot receive incoming connections operate in polling mode: they periodically call GET /sync on reachable peers to retrieve new messages. This is a first-class operating mode, not a fallback.

The campfire has no infrastructure beyond the members themselves. The campfire is as available as its most available member. If all members go offline, the campfire is dormant. When any member comes back and can reach another member, the campfire resumes.

Beacons for P2P HTTP campfires include one or more member endpoints (not a relay URL). Any member can publish a beacon with their own endpoint. Multiple beacons for the same campfire can coexist.

Join flow for P2P HTTP:

  1. New member discovers a beacon and contacts a listed member endpoint.
  2. Contacted member checks join protocol, admits the new member.
  3. Contacted member sends: campfire key material (full key for threshold=1, key share for threshold > 1, encrypted to the new member's public key), plus the full member list with endpoints.
  4. Contacted member notifies all other members of the new join.
  5. New member is now a peer — knows all members, can send to all, can receive from all.

Threshold signing for P2P HTTP (threshold > 1): When a member sends a message and needs a provenance hop signature, they initiate a signing round with M - 1 other members. Each participant contributes their key share to produce a valid Ed25519 signature on the hop. The signing round adds latency (one network round-trip between signers). For small campfires with low threshold, this is milliseconds. For large campfires with high threshold, this is the primary latency cost.

Security Considerations

Identity spoofing. All messages are signed. A recipient verifies the sender's signature against their known public key. A provenance chain with an invalid signature at any hop is rejected entirely.

Membership snapshot verification. Provenance hops include a Merkle hash of the membership set. Any member can request the full set from the campfire and verify the hash. A campfire that lies about its membership in provenance hops can be detected by any member that independently verifies.

Malicious campfire (threshold = 1). When any single member can sign provenance hops, a compromised member can fabricate hops. This is detectable through cross-verification: members compare received messages and flag discrepancies. The protocol trusts campfires with threshold = 1 to honestly relay. A campfire's reputation is its track record of honest provenance. For campfires requiring stronger guarantees, use threshold > 1.

Threshold security (threshold > 1). A compromised member holds only a key share and cannot forge provenance hops alone. An attacker must compromise M members to forge a signature. The threshold is the security parameter — campfires choose the tradeoff between latency (higher threshold = more signers = more latency) and security (higher threshold = more members must be compromised).

Split prevention. A campfire split (eviction dispute, network partition) with threshold > 1 has an unambiguous resolution: the partition with M or more members can sign, the other cannot. The signing threshold is the quorum. For threshold = 1, splits require rekey (see Eviction and Rekey) and the campfire:rekey message establishes succession.

Private campfire confidentiality. In a two-member campfire (private conversation), only the two members and the campfire itself see message content. Messages can be encrypted with the recipient's public key for end-to-end confidentiality, making the campfire a blind relay. The protocol doesn't mandate encryption but the identity system supports it.

Agent reachability. Agents have no standalone address. They are reachable only through campfires they belong to. An agent that leaves all campfires is unreachable. This is a feature: leaving all campfires is how an agent goes dark.

Trust manipulation. Trust level is derived from vouches by other members. A member cannot inflate their own trust level — self-vouches are excluded. A member cannot inflate another member's trust level beyond one vouch per voucher. Sybil attacks (creating fake members to vouch) are bounded by the join protocol: in invite-only or delegated campfires, each fake member must be admitted by a real member, and admitters who bring in noisy or colluding members are subject to eviction. In open campfires, sybil resistance is weaker — open campfires should not rely on trust level for critical access control. When a trusted member is compromised, their trust level persists until other members send campfire:revoke messages. Trust reflects historical reputation, not real-time security state. Revocation is the response to compromise.

Reserved tag enforcement. Tags in the campfire: namespace (except campfire:vouch, campfire:revoke, and campfire:invite) must be signed by the campfire key. A member cannot forge system messages (campfire:member-joined, campfire:rekey, campfire:eviction, etc.). The three exceptions are verified against member keys and cannot be used to impersonate the campfire.

Provenance chain depth. There is no protocol-level limit on provenance chain depth. Deep chains are a natural consequence of recursive composition. Receivers MAY drop messages with unverifiable provenance (missing intermediate campfire keys) as a local filter decision — provenance depth and verifiability are protocol-derived properties available as filter inputs. Truncation has consequences: a message whose path cannot be traced back to a known origin is indistinguishable from a forged message.

Antecedent references. Antecedents are claims, not proofs. A message can reference a message ID the recipient has never seen — the referenced message may live in another campfire, may not have been relayed yet, or may not yet exist (futures). The antecedents field is covered by the sender's signature and cannot be tampered with in transit. A malicious sender could reference nonexistent message IDs, but this is no different from sending misleading payload content — the protocol authenticates the sender, not the truth of their claims.

Membership Roles

Membership in a campfire carries one of three roles. Implementations MUST enforce role permissions before message delivery. Transport-level enforcement is OPTIONAL; client-side enforcement is REQUIRED.

Role Definitions

Role Send regular messages Send campfire:* system messages Read messages
observer No No Yes
writer Yes No Yes
full Yes Yes Yes
blind-relay Yes (forward only) No No (encrypted content)

observer. Read-only membership. An observer receives all messages but cannot send. Attempting to send from an observer role returns a role enforcement error. Suitable for audit members, monitoring agents, and silent listeners.

writer. Read-write membership for regular messages. A writer can send messages with any non-system tags. Attempting to send a message with any campfire:* tag returns a role enforcement error. Suitable for most participating members.

full. Full access. A member with full role can send regular messages, sign and emit campfire:* system messages, change member roles, and run compaction. This is the default for backward compatibility.

blind-relay. A relay member that stores and forwards messages but cannot read encrypted content. A blind relay signs provenance hops with role "blind-relay" (a verified field in the hop signature). Bridge transports set this role on forwarded messages via a per-hop role annotation — the bridge's membership role is unchanged, only the provenance hop is marked. Messages with at least one blind-relay hop in their provenance chain are considered bridged (see IsBridged() in the SDK). Blind-relay hops elevate the message's operator provenance to Level 2 (Contactable) for convention gate evaluation.

EffectiveRole and Backward Compatibility

Implementations MUST map raw role strings to canonical values:

  • "observer"observer
  • "writer"writer
  • "full"full
  • "blind-relay"blind-relay
  • Any other value (empty string, "member", "creator", unknown legacy values) → full

Existing memberships with no role field or pre-role-system role values automatically resolve to full, preserving backward compatibility without requiring migration.

Role Assignment

Roles are set at join time (the joining member receives a role) or changed afterward by a full member. A member cannot change their own role. Only full members can issue role changes.

Role Change System Message

When a role is changed, the campfire emits a campfire:member-role-changed system message signed by the campfire's own key:

campfire:member-role-changed payload {
  member:        hex-encoded public key of the member whose role changed
  previous_role: prior role string
  new_role:      new role string
  changed_at:    unix nanosecond timestamp of the change
}

This message is signed by the campfire key (not the caller's member key), making it a verified system event. It is not signed by the calling member, so receivers can trust the campfire attested the change — not just the requesting member's claim.

Enforcement Model

Implementations MUST enforce role permissions before message delivery:

  • Observer roles MUST NOT be permitted to send any message
  • Writer roles MUST NOT be permitted to send messages with any campfire: tag
  • Only full role members may send campfire:* system messages, change member roles, or run compaction

Transport-level enforcement (rejecting messages at the protocol boundary) is OPTIONAL. Client-side enforcement is the minimum requirement.

Compaction

Campfire message stores grow without bound. Compaction is a protocol-level operation that marks a set of messages as superseded by a summary, allowing new members to bootstrap from the summary rather than the full history.

Append-only semantics. Compaction does not delete messages. It appends a campfire:compact event that declares which messages are superseded. The superseded messages remain in the store. Implementations MAY discard them locally (retention policy discard) or archive them (retention policy archive). The compaction event itself is permanent.

New-member snapshot. When a new member joins after a compaction, they start from the compaction event (the snapshot) rather than replaying the full message history. The summary field provides a human- and agent-readable description of the compacted content. The checkpoint_hash provides a cryptographic digest for integrity verification.

campfire:compact Event Structure

A compaction event is a regular campfire message with tag campfire:compact. The payload is JSON:

campfire:compact payload {
  supersedes:      [message-id, ...]   # IDs of messages superseded by this compaction
  summary:         bytes               # human/agent-readable description of compacted content
  retention:       "archive" | "discard"  # hint to implementations about local storage
  checkpoint_hash: hex-string          # SHA-256 of sorted(id + "|" + hex(signature)) for each superseded message
}

The antecedents of the compaction event contains the ID of the last superseded message, establishing the causal boundary.

supersedes. The complete list of message IDs that this compaction event covers. Implementations use this list to identify which messages to exclude from default reads.

summary. Freeform bytes describing what the compacted messages contained. This is the snapshot content: the full semantic value that new members need instead of the raw history. May be structured (JSON) or plain text.

retention. A hint to local implementations. archive means keep the superseded messages locally but exclude from default reads. discard means the messages may be deleted after compaction. The campfire cannot enforce this — it is an implementation hint.

checkpoint_hash. A deterministic hash of all superseded messages for integrity verification. Computed as SHA-256 of the sorted list of {id}|{hex(signature)} entries for each superseded message. Recipients can recompute this hash from local storage to verify the compaction event is consistent with the messages it references.

Compaction Semantics

  • Only full role members may send campfire:compact (it is a system tag)
  • Compaction events themselves are never superseded by other compaction events
  • Implementations SHOULD exclude superseded messages from default read operations and provide a mechanism to include them
  • Multiple compaction events may coexist; their supersedes lists are union-ed
  • A compaction event supersedes a specific set of messages by ID — it does not invalidate messages sent after it

campfire:compact and Reserved Tags

campfire:compact is a reserved system tag. Messages with this tag MUST be signed by a member with full role.

Named Views

A named view is a persistent predicate that filters and shapes message results. Views are defined by sending a campfire:view message into the campfire. Any member can materialize a view by evaluating the predicate against the message store. Views are query definitions stored as messages — they are not caches or pre-computed results.

campfire:view Event Structure

A view definition is a regular campfire message with tag campfire:view. The payload is JSON:

campfire:view payload {
  name:       string             # unique identifier for the view within the campfire
  predicate:  string             # S-expression predicate string (see Predicate Grammar below)
  projection: [field-name, ...]  # optional: field names to include in output (omit = all fields)
  ordering:   string             # "timestamp asc" (default) or "timestamp desc"
  limit:      int                # maximum result count; 0 = no limit
  refresh:    string             # "on-read" (only strategy supported in P1)
}

name. The view's identifier within the campfire. Later campfire:view messages with the same name supersede earlier ones — the latest definition wins. Names are case-sensitive.

predicate. An S-expression predicate string evaluated against each message's context. See Predicate Grammar below.

projection. A list of field names to include in output. Valid field names: id, sender, instance, payload, tags, antecedents, timestamp, signature, provenance, campfire_id. If omitted or empty, all fields are returned.

ordering. Result ordering. Default is "timestamp asc" (natural message order). "timestamp desc" reverses order, useful for "most recent N" queries with a limit.

limit. Maximum number of messages to return after filtering and ordering. 0 means no limit.

refresh. How and when the view's results are computed. Implementations MUST support "on-read" (re-evaluate on each materialization). "on-write" (pre-compute on message arrival) and "periodic:duration" (re-evaluate on a schedule) are OPTIONAL.

View Materialization Semantics

  • Views exclude campfire:* system messages from results. This is critical for negation predicates: (not (tag "foo")) must not match view definitions or compaction events.
  • Views respect compaction by default: superseded messages are excluded from view results.
  • Views evaluate the predicate against all non-system, non-superseded messages in the campfire, regardless of read cursor.
  • Only full role members may create views (campfire:view is a system tag).

Predicate Grammar

Predicates use S-expression syntax. The grammar is:

predicate := boolean-expr | comparison-expr
boolean-expr := (and pred pred ...)     ; at least 2 arguments; short-circuit evaluation
              | (or  pred pred ...)     ; at least 2 arguments; short-circuit evaluation
              | (not pred)
comparison-expr := (gt  value-expr value-expr)
                 | (lt  value-expr value-expr)
                 | (gte value-expr value-expr)
                 | (lte value-expr value-expr)
                 | (eq  value-expr value-expr)
leaf-expr := (tag "string")            ; true if message has this tag (case-insensitive)
           | (sender "hex-prefix")     ; true if sender hex starts with prefix (case-insensitive)
           | (field "dot.path")        ; extract JSON field from payload; "payload." prefix is optional
           | (mul value-expr value-expr)
           | (pow value-expr value-expr)
           | (literal number)          ; numeric literal
           | (literal "string")        ; string literal
           | (timestamp)               ; message timestamp in unix nanoseconds

Operator Reference

Operator Arity Arguments Returns Notes
and N≥2 boolean expressions boolean Short-circuit: returns false at first false child
or N≥2 boolean expressions boolean Short-circuit: returns true at first true child
not 1 boolean expression boolean Logical negation
tag 1 quoted string boolean Case-insensitive tag membership test
sender 1 quoted hex string boolean Case-insensitive prefix match on sender hex
gt 2 numeric expressions boolean left > right
lt 2 numeric expressions boolean left < right
gte 2 numeric expressions boolean left >= right
lte 2 numeric expressions boolean left <= right
eq 2 numeric or string expressions boolean String equality when both operands are strings; numeric equality otherwise
field 1 quoted dot-path string value Extracts nested JSON field from parsed payload; returns numeric, string, or bool depending on JSON type; returns empty result if field absent or payload not JSON
mul 2 numeric expressions numeric Multiplication
pow 2 numeric expressions numeric Exponentiation: base ^ exponent
literal 1 number or quoted string value Numeric literal if parseable as float64; string literal otherwise
timestamp 0 numeric Message timestamp in unix nanoseconds

Field Path Resolution

(field "path") extracts a value from the message's JSON payload using dot-notation. The prefix payload. is optional and stripped if present — (field "confidence") and (field "payload.confidence") are equivalent.

Nested fields: (field "outer.inner.value") navigates payload["outer"]["inner"]["value"]. If any segment is absent or the payload is not valid JSON, the result is empty (evaluates to false in boolean context, 0 in numeric context).

Predicate Depth Limit

The evaluator enforces a maximum recursion depth of 64 nodes. Predicates exceeding this depth return false rather than an error, preventing malicious predicates from causing stack overflows.

Example Predicates

; Messages tagged "memory:standing"
(tag "memory:standing")

; Messages tagged either "memory:standing" or "memory:anchor"
(or (tag "memory:standing") (tag "memory:anchor"))

; Messages tagged "memory:standing" with confidence above 0.5
(and (tag "memory:standing") (gt (field "confidence") (literal 0.5)))

; Messages from a specific sender prefix
(sender "abc123")

; Messages tagged "status" but not "draft"
(and (tag "status") (not (tag "draft")))

; Messages after a specific timestamp
(gt (timestamp) (literal 1710000000000000000))

Reserved Tags: Extended Namespace

The following campfire:* tags are defined as of this revision. All require full membership role to send and are verified against the campfire's key by receivers.

Tag Description Introduced in
campfire:member-joined A new member has joined P0
campfire:member-evicted A member was evicted P0
campfire:member-left A member voluntarily departed P0
campfire:eviction Companion tag on eviction messages P0
campfire:rekey Campfire rotated its keypair P0
campfire:disband Campfire is being disbanded P0
campfire:invite Invitation to join a campfire P0 (member-signed exception)
campfire:vouch Trust vouch for a member key P0 (member-signed exception)
campfire:revoke Revocation of a prior vouch P0 (member-signed exception)
campfire:compact Compaction event — marks messages superseded P1
campfire:view Named view definition P1
campfire:member-role-changed A member's role was changed P1
campfire:visibility-changed Campfire flipped from public to private 0.30
session:open Session campfire opened — lifecycle anchor alongside campfire:rekey 0.30
session:close Session campfire closed — lifecycle anchor alongside campfire:rekey 0.30

Member-signed exceptions. campfire:invite, campfire:vouch, and campfire:revoke are signed by individual members, not the campfire key. All other campfire:* tags are signed by the campfire.

P1 additions. campfire:compact, campfire:view, and campfire:member-role-changed were introduced in the automaton substrate feature set. They extend the reserved namespace to cover log management (compact), persistent query definition (view), and role governance (member-role-changed).

0.30 additions. campfire:visibility-changed, session:open, and session:close were introduced in the cf 0.30 Layer-1 freeze. See §cf-protocol 1.0 Layer-1 Additions below.

Ratified L1 reserved tags (non-campfire: namespace). The following tags were de facto reserved since P0 and are now explicitly enumerated as part of the cf-protocol 1.0 freeze. They are member-signed (covered by the sender's signature), not campfire-key-signed:

Tag Description Introduced in
future Commitment pending fulfillment — a message the sender expects will be satisfied P0 (ratified 0.30)
fulfills Satisfies a prior future — must include the future's ID in antecedents P0 (ratified 0.30)

cf-protocol 1.0 Layer-1 Additions

This section documents the Layer-1 additions ratified as part of the cf-protocol 1.0 freeze for cf 0.30. All items in this section are wire-format frozen: implementations MUST NOT alter their semantics without a major-version bump to cf-protocol.

campfire:visibility-changed

campfire:visibility-changed is a campfire-key-signed system event. It fires when a campfire transitions from public to private (i.e., its join protocol changes from open to invite-only). The tag requires the campfire's own key for signing — it is NOT a member-signed exception. Receivers MUST reject any campfire:visibility-changed message whose signature does not verify against the campfire's public key.

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
}

Semantics. The event fires exactly once per public→private transition. The campfire emits the event before enforcing the new policy so that currently-connected members can observe the change and update their records. The event does not imply member eviction — existing members retain their membership unless separately evicted.

Consumer obligations. Members MUST treat this event as authoritative for the campfire's current join policy. Agents that cache join-protocol state MUST invalidate their cache on receipt. Discovery components (beacons, named entries) MUST reflect the updated policy at their next refresh. Agents that advertise the campfire's join protocol to third parties MUST NOT continue advertising open after observing this event. Failing to respond to campfire:visibility-changed exposes downstream agents to a stale join-policy attack (a campfire claims to be open after going private).

Placement. Sits at L1 alongside campfire:rekey in the system-event vocabulary. Closes OPEN-016.

session:open / session:close

session:open and session:close are campfire-key-signed L1 system events that bracket the lifecycle of an ephemeral session campfire. Both tags require the campfire's own key for signing — they are NOT member-signed exceptions. Receivers MUST reject any session:open or session:close message whose signature does not verify against the session campfire's public key.

session:open 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 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"
}

Semantics. session:open is emitted by the session creator (orchestrator) when a new session campfire is initialized. It anchors the session at the human chain root and records the capability bounds for all worker grants issued under the session. session:close is emitted by the orchestrator when a session ends — either because until has elapsed or the orchestrator explicitly ends the session. Session campfire messages may be compacted aggressively after session:close (default operator policy: 24h retention after close). Grant lineage survives compaction because issued delegation:grant messages live in the owner's identity campfire, not in the session campfire — see §Client.Await Contract below for the fulfillment ordering rule that governs grant-backed futures.

Placement. Sits at L1 alongside campfire:rekey and campfire:visibility-changed in the system-event vocabulary. Closes OPEN-004 §3.

future / fulfills Reserved Tags — L1 Freeze Ratification

The "future" and "fulfills" tags are ratified as Layer-1 frozen reserved tags. They were de facto reserved at P0 (see §Futures and Fulfillment above) but are now explicitly enumerated as part of the cf-protocol 1.0 freeze. Their DAG semantics — the antecedents field as a CBOR-frozen typed array of message IDs, the "future" tag meaning "commitment pending fulfillment," and the "fulfills" tag meaning "this message satisfies the referenced future" — are frozen together with the message envelope and cannot be split into a separate layer-3 convention.

Why now. Three constraints require explicit ratification at 1.0 freeze time:

  1. Wire format already fuses them. Antecedents []string sits at CBOR field 5 of the signed envelope alongside payload, tags, and timestamp. The sender's signature covers the antecedents bytes whether a receiver projects them or not. A "message-only" consumer is already verifying antecedent bytes.

  2. Client.Await is a Layer-1 read primitive alongside Send and Read. It reads the "fulfills" tag and antecedents directly from the store without convention-layer mediation. Fulfillment is not optional projection — it is a first-class read operation.

  3. Every named consumer uses both. rd, dontguess, social, the reach, freeso, and hosted customers all use antecedents and fulfillment together with the message envelope. The speculative "leaner consumer" who wants signed event log without DAG does not exist, and per the standing rule no protocol surface is justified by an unnamed consumer.

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. Convention authors may declare antecedents: none for their operations; this is convention-level behavior configuration, not a Layer-1 carve-out of structure.

The reserved-tag table is updated to include "future" and "fulfills" alongside the campfire:* namespace. These tags do not carry the campfire: prefix and are therefore member-signed (the sender's key covers them), not campfire-key-signed. Closes OPEN-001.

Reserved-Op Floor LIST

The following ten operations form the reserved-op floor: a protocol-level minimum that no convention declaration and no parent grant can lower. Convention authors are not in the trusted computing base for these operations. The clamping order is fixed and one-directional: owner ceiling > parent grant > convention declaration. No convention may declare access to a reserved op without a grant from a principal with owner-level authority in the relevant campfire.

Canonical list:

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 dispatch interceptor); the EVALUATOR (applies actual grant-chain policy) lives at Layer 3 cf-authority. Convention declarations that reference a reserved op are validated against this list at parse time. Any op that appears in the list but is absent from a caller's grant chain results in DENY(reserved-op-floor). Closes OPEN-003.

Antecedent/Fulfillment Fusion

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. cf-protocol 1.0 freezes them together with the envelope, signatures, and hop chain — 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.

Rationale (closes OPEN-001). Three constraints rule out separation. (1) Wire-format reality: the antecedents bytes are in the signed envelope; a "message-only" consumer either projects them or ignores them, and ignoring them invites chain-fabrication attacks that the threat model defends against. (2) Client.Await reads "fulfills" tags and antecedents at Layer 1 today — fulfillment is not optional projection. (3) Every named portfolio consumer uses antecedents and fulfillment together with messages. The speculative "leaner consumer" who wants signed event log without DAG is not named, and per the standing rule no carve-out is justified by an unnamed consumer.

What this does not say. 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. A consumer that receives a message with unfulfilled antecedents MAY wait, act speculatively, or ignore antecedent state. The graph is data; interpretation is local.

Client.Await Contract

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

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 antecedents array.

Both conditions are required. A message that carries "fulfills" but does not reference the target ID in antecedents is NOT a fulfillment of that future. A message that references the target ID in antecedents but does not carry "fulfills" is a dependent, not a fulfillment.

Fulfillment ordering — earliest-timestamp wins. When multiple messages fulfill the same future (concurrent fulfillments), 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. The winning message is returned; the other fulfillments remain in the campfire log and are accessible via direct read. Implementations MUST apply this ordering rule — selecting a random fulfillment or returning all fulfillments is a protocol violation.

Why deterministic ordering matters. Futures used for escalation (agent awaits a ruling) and for session-worker grant issuance (orchestrator awaits identity:introduce) both have at most one legitimate fulfillment in practice. The ordering rule handles the adversarial case where a malicious actor sends a second "fulfills"-tagged message to race the legitimate fulfillment. Because earliest-timestamp wins, the legitimate fulfillment (posted first by the authorized principal) wins. The tiebreaker on message ID ensures the rule is fully deterministic even when an adversary manufactures a message with an identical timestamp.

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

Wire Format

Not specified in this version. The protocol defines the logical structure of messages, provenance chains, and membership data. Serialization format (protobuf, msgpack, CBOR, JSON) is an implementation choice. The only requirement is that the serialization is deterministic for signature verification.

CLI Reference (Implementation Sugar)

The protocol is independent of any CLI. The following commands are suggested sugar for implementations targeting AI agents and developers.

cf init                              # generate keypair, create agent identity
cf discover [--channel fs|dns|git|mdns] [--tag t] [--description s]  # list beacons (tainted fields withheld by default)
cf create [--protocol open|invite-only] [--require tag,...] [--tag t,...] [--transport proto] [--beacon channel]
cf join <campfire-id>                # request to join
cf admit <campfire-id> <member-key>  # sponsor a new member
cf invite <target-key> <campfire-id> # send invitation through a shared campfire
cf evict <campfire-id> <member-key>
cf leave <campfire-id>
cf disband <campfire-id>
cf vouch <campfire-id> <member-key>   # vouch for a member
cf revoke <campfire-id> <member-key>  # revoke a vouch
cf send <campfire-id> "message" [--tag tag,...]
cf dm <target-key> "message"         # sugar: create/reuse 2-member campfire, send
cf read [campfire-id]                # read messages, optionally filtered to one campfire
  --all                              # show all messages, not just unread
  --peek                             # show unread messages without updating cursor
  --follow                           # stream messages in real time
  --tag <tag> [--tag <tag> ...]      # filter by tag (OR semantics; SQL-level)
  --sender <hex-prefix>              # filter by sender prefix
  --fields <field,...>               # project: comma-separated subset of id,sender,instance,payload,tags,timestamp,antecedents,signature,provenance,campfire_id
  --pull <id[,id,...]>               # fetch specific messages by ID from local store
cf inspect <message-id>              # show full provenance chain
cf ls                                # list my campfires
cf members <campfire-id>
cf id                                # show my public key
cf compact <campfire-id>             # create campfire:compact event (full role required)
  --before <msg-id>                  # compact messages before this ID (default: all)
  --summary "text"                   # human-readable summary of compacted content
  --retention archive|discard        # local storage hint (default: archive)
cf view create <campfire-id> <name>  # create named view (full role required)
  --predicate <s-expr>               # S-expression predicate (required)
  --projection <field,...>           # field names to include in output
  --ordering "timestamp asc|desc"    # result ordering (default: timestamp asc)
  --limit <n>                        # max results (default: 0 = no limit)
cf view read <campfire-id> <name>    # materialize a named view
cf view list <campfire-id>           # list all defined views in a campfire
cf member set-role <campfire-id> <pubkey> --role observer|writer|full  # change member role (full role required)