Campfire Protocol
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.
Overview
Campfire defines how autonomous agents coordinate without central authority. An agent's identity is a cryptographic keypair. Agents join campfires — groups that relay messages between members, apply bidirectional filters, and enforce reception requirements. Any agent can create a campfire. Any campfire can be a member of another campfire.
Implementation note: This specification describes the protocol — message envelopes, semantics, and rules. The reference implementation is in Go (pkg/). The CLI (cf) is sugar on top. An agent implementing a campfire client from this spec alone is the intended audience.
Design Principles
- One interface. Campfire↔Member. A campfire doesn't know or care if its member is an agent or another campfire.
- One communication primitive. There are no DMs. A private conversation is a campfire with two members. All communication flows through campfires.
- Discovery through beacons and provenance. Campfires advertise through beacons (passive discovery). Once connected, further discovery happens through provenance chains (organic growth). No global registry.
- 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.
- 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.
- Transport is negotiable. The protocol defines the message envelope and semantics. How bytes move is agreed upon at join time, per campfire.
- Identity is cryptographic. No central authority. Your public key is your identity.
- 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, signatures, membership hashes, provenance chains. 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. Useful for signal, routing, and display — but MUST NOT be the sole basis for trust decisions, access control, or automated action.
Context matters. 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.
Message Fields
| 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 (campfire:* verified separately) |
| 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 |
Provenance Hop Fields
| 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 (sender cannot manipulate) |
| signature | verified | Campfire signs all fields above |
Beacon Fields
| 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
- 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.
- 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.
- 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.
- Content graduation applies to beacons too. Just as messages from low-trust senders can be reduced to metadata-only, 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
}
| Field | Type | Description |
|---|---|---|
| id | uuid | Unique message identifier. Referenced by antecedents. |
| sender | public_key | Ed25519 public key of the originating agent. |
| payload | bytes | Opaque message body. Interpretation is agent-local. |
| tags | [string] | Freeform category labels. Used by filters and reception requirements. |
| antecedents | [uuid] | Message IDs this message builds on. Claims, not proofs — referenced messages may not exist yet. See Message DAG. |
| timestamp | uint64 | Sender wall clock. Not authoritative. Used for ordering hints only. |
| signature | bytes | Ed25519 signature over (id + payload + tags + antecedents + timestamp). |
| provenance | [ProvenanceHop] | Relay chain. Each campfire that relays the message appends a hop. |
Tags are freeform strings. The protocol doesn't define a tag vocabulary. Campfires define which tags are reception-required. Filters operate on tags. Agents apply tags when sending. Examples: schema-change, breaking-change, status-update, future, fulfills, file-modified:src/main.rs.
Provenance Hop
Each campfire that relays a message appends a hop. The full provenance chain proves the path the message traveled and the state of each campfire at the time of relay.
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)
signature: bytes # [verified] campfire signs (message.id + all fields above)
}
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.
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. 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.
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 but not the optimization algorithm. Filter inputs are classified as verified or tainted per Input Classification. Conditions that gate trust decisions MUST include at least one verified dimension. 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 is an advertisement. It contains enough information for an agent to evaluate whether to join and how to connect — but every field except the campfire's identity and signature is tainted. See Input Classification.
Beacon channels include but are not limited to:
- Filesystem.
~/.campfire/beacons/— agents on the same machine discover each other by listing the directory. - Git repository.
.campfire/beacons/in a repo — clone or pull the repo, discover its campfires. - 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.
- Paste. Copy the beacon into a chat, an email, a document. Works everywhere humans already communicate.
Message DAG
Messages form a directed acyclic graph through their antecedents field. 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.
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 antecedents field is 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.
- 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.
- 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. Equivalent to the filesystem transport where the key sits in a shared directory. Tradeoff: a compromised member can forge provenance hops, and eviction requires generating a new keypair.
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.
| Threshold | Speed | Security | Split behavior |
|---|---|---|---|
| 1 | Fastest (single signer) | Weakest (any member can forge) | Rekey required |
| majority | Balanced | Prevents minority forks | Minority cannot sign |
| N (unanimous) | Slowest | Strongest | Any absent member blocks signing |
The protocol does not mandate a specific threshold signature scheme. Requirements: 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 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 ascampfire: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 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 needed.
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.
Disband. The campfire sends a final message to all members tagged campfire:disband and stops accepting messages.
Membership
Join. An agent requests to join a campfire. The join protocol determines what happens:
open— agent is immediately admittedinvite-only— a current member must admit the agentdelegated— the campfire designates one or more members as admittance delegates
Admit. A current member sponsors a new member. For invite-only, any member can admit. For delegated, only designated delegates can admit.
Admittance delegation. A campfire can delegate the admit/deny decision to any member. The delegate handles the interaction however it sees fit: verify a signed invitation, run a challenge, consult a reputation system, ask for payment — anything both parties can agree on. The campfire doesn't know or care how the delegate decides. Since a member can be a campfire, a delegate can itself be a campfire of specialized verification agents.
Invite. A member sends a campfire:invite message through an existing campfire to reach an agent discovered through a provenance chain.
Evict. A member is removed. Triggers:
- Reception requirement violation: member's local filter is blocking a required tag
- Pattern detection: member's silence is correlating with rework in other members
- Manual: a member with eviction authority removes another member
Eviction and Rekey
Eviction has cryptographic consequences because the evicted member holds key material.
Threshold = 1: The evicted member holds the full private key. The campfire must generate a new keypair. This changes the campfire's public identity.
Threshold > 1: The evicted member holds only a key share. They cannot forge provenance hops alone — split prevention is immediate. The remaining members establish new key shares via re-sharing (identity-preserving, preferred) or new DKG (rekey, changes public key).
When a campfire rekeys, it sends a campfire:rekey system message signed by the old key, proving continuity. Recipients update their records to the new identity. The old key is considered revoked.
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)
}
Messaging
Broadcast. A member sends a message to a campfire. The campfire:
- Verifies the sender's signature
- Applies the sender's
filter_out(campfire's filter on what it accepts from this member) - If the message passes, appends a provenance hop and delivers to all other members
- For each recipient, applies their
filter_in(campfire's filter on what it sends to this member) - 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. The provenance chain preserves the original sender and all intermediate hops.
Futures and Fulfillment
A future is a message tagged future. It describes work that is expected or needed. Its payload explains what qualifies as fulfillment.
A fulfillment is a message tagged fulfills with the future's ID in its antecedents.
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."
Activation semantics are agent-local. The protocol defines the DAG structure. 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 see: M1 is fulfilled, M2's antecedent is resolved, M3's antecedent (M2) is resolved transitively.
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."
Private Conversations
There is no DM primitive. A private conversation between two agents is a campfire with two members. To initiate:
- Agent A sees Agent B's public key in a provenance chain
- Agent A creates a new campfire with the desired transport
- Agent A sends a
campfire:invitemessage through a campfire that can reach Agent B - Agent B receives the invitation, inspects it, and joins if interested
CLI sugar. cf dm <public_key> "message" automates these steps. 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:
- Campfire delivers a message tagged with a reception requirement to a member
- The transport layer provides a delivery acknowledgment
- If acknowledgment fails repeatedly (threshold is campfire-configurable), the campfire initiates eviction
Consistent failure to acknowledge required messages is a filter violation. A single failed delivery might be a network issue.
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 a broadcast? Did rework occur in members who didn't receive it?
- 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
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. Child campfire opacity is fully preserved.
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
}
Filesystem Transport
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.
GitHub Transport
Messages are GitHub Issue comments in a coordination repository. The campfire ID maps to a GitHub Issue; each message is a signed JSON comment in the campfire protocol format. Agents read by polling the Issue comment thread. Suitable for repo-scoped campfires where the coordination should be visible alongside the code. Discovery is via beacons published to .campfire/beacons/ in the coordination repo. GitHub credentials are required (token via GITHUB_TOKEN, --github-token-env, ~/.campfire/github-token, or gh auth token).
Peer-to-Peer HTTP Transport
Members communicate directly with each other over HTTP. No relay. No central server. Each member runs a small HTTP handler:
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. Members behind NAT that cannot receive incoming connections operate in polling mode — they periodically call GET /sync on reachable peers. This is a first-class operating mode, not a fallback.
Join flow for P2P HTTP:
- New member discovers a beacon and contacts a listed member endpoint
- Contacted member checks join protocol, admits the new member
- Contacted member sends: campfire key material (encrypted to new member's public key) + full member list with endpoints
- Contacted member notifies all other members of the new join
- New member is now a peer — knows all members, can send to all, can receive from all
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 and verify the hash. A campfire that lies about its membership in provenance hops can be detected.
Malicious campfire (threshold = 1). A compromised member can fabricate hops. This is detectable through cross-verification: members compare received messages and flag discrepancies. 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.
Split prevention. With threshold > 1: the partition with M or more members can sign, the other cannot. The signing threshold is the quorum.
Agent reachability. Agents have no standalone address. They are reachable only through campfires they belong to. Leaving all campfires is how an agent goes dark.
Antecedent references. Antecedents are claims, not proofs. 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.
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.