Carbon Copy¶
Carbon Copy (CC) is Foundation Protocol's owner-observability mechanism. Every time an entity sends or receives a message, a copy is automatically forwarded to that entity's owner. The owner — usually a human supervising a fleet of agents — can watch every conversation their entities are having.
CC is built on the same Message and Mail primitives as any other interaction. It is not a side channel.
Owners and observability¶
Every entity may declare an owner (FPAddress). When an entity has an owner, the runtime generates one CC per direction:
| Direction | Generated by | Triggered when |
|---|---|---|
outbound |
Sender entity | The owned entity sends a message |
inbound |
Recipient entity, via CarbonCopyCheckpoint |
The owned entity receives a message |
Both copies share the same original_message_id, so the owner's UI can dedupe and pair them.
Payload¶
# fp/message.py
class CarbonCopyPayload(BaseModel):
original_sender: str # host_uid:entity_uid
original_sender_name: str
original_recipient: str
original_recipient_name: str
original_kind: str # the kind of the underlying message
original_message_id: str # links the two CCs together
direction: str # "outbound" | "inbound"
timestamp: str # ISO-8601 UTC
cost: float | None = None # e.g. LLM token cost, if metered
summary: str | None = None # first ~100 chars of the original payload
CC messages use MessageKind.CARBON_COPY and travel through the normal entity.send_message path — they are signed, routed, and stored just like any other Mail.
Generation paths¶
Outbound — at the sender¶
Entity.send_message (fp/entity.py) checks three conditions before generating a CC:
- The entity has an
owner. - The message being sent is not itself a CC (no recursion).
- The recipient is not the owner (the owner can already see direct messages).
When all three hold, the sender entity emits a CarbonCopyPayload(direction="outbound") to the owner.
Inbound — at the recipient¶
CarbonCopyCheckpoint.execute (fp/core/checkpoint.py) intercepts every inbound message. Two cases:
Case 1 — the inbound message is itself a CC. The checkpoint formats it for logging, pushes it to the web UI if the entity is HUMAN, and short-circuits with handled_success() — no handler runs.
Case 2 — a normal inbound message. If the recipient has an owner and the sender is not the owner, the checkpoint builds a CarbonCopyPayload(direction="inbound") and sends it to the owner, then returns success() so the normal pipeline continues.
When CC is skipped¶
| Condition | Path | Why |
|---|---|---|
entity.owner is None |
both | No one to copy |
message.kind == CARBON_COPY |
outbound | Prevent CC of a CC |
recipient == owner |
outbound | Owner already sees it |
sender == owner |
inbound | Owner already knows what they sent |
Example — two owned agents talk¶
GYF (a human) owns two agents, MyClaude-1 and MyCodex-1. MyClaude-1 sends a message to MyCodex-1:
MyClaude-1 → MyCodex-1 ("hello")
1. MyClaude-1.send_message(to=MyCodex-1, ...)
├─ routes the message to MyCodex-1
└─ emits outbound CC → GYF
(direction=outbound, original_sender=MyClaude-1)
2. MyCodex-1 receives the message
└─ CarbonCopyCheckpoint runs
└─ emits inbound CC → GYF
(direction=inbound, original_sender=MyClaude-1)
3. GYF receives two CCs sharing the same original_message_id:
- one from MyClaude-1 (outbound)
- one from MyCodex-1 (inbound)
GYF's UI groups them by original_message_id and shows the conversation from both sides.
Delivery to the UI¶
The Carbon Copy mechanism is protocol-level; the UI is a separate concern, but the integration is straightforward:
- Hosts forward inbound CCs to connected web clients over WebSocket.
- Clients that miss a WebSocket update fall back to polling the entity's mailbox for messages of
kind == "carbon_copy". - The CC is identified by its payload structure — no special envelope handling required.
This means any FP host implementation gets CC for free as soon as it implements the standard message envelope and the CarbonCopyCheckpoint (or an equivalent).