Chio/Docs
LOGIN · JOIN

Mailbox Attestation

Read which process the kernel recorded as a message's sender, what that record proves, and how to handle a null sender.

Concept chio@5f63e6e

On this page

A consumer can act on a mailbox message knowing which admitted process sent it. The kernel resolves the sender from the caller's own credential and stores it beside the message, so a payload that names its own author changes nothing.

Processes on one host exchange work through mailboxes, durable capability-addressed queues the kernel serves as native tools. In the running example the fixer sends issue-417 to the reviewer with a payload that falsely names tester as its author, and the reviewer still reads fixer.

The kernel names the sender, and the payload cannot

An embedded host attaches a ProcessRegistry through MailboxServer::attest_senders, and dispatch resolves the calling process before it serves a send, claim, complete, or renew. The send schema declares only message_key and payload, so no caller can set a sender, on the wire or anywhere else.

send_jobs · issue-417declared_sender:testerp1fixerp2testerKProcessRegistryrecordsmjobsissue-417sender fixerseq nreceive_jobsp3reviewersend_jobsp1fixerp2testerKregistryrecordsmjobsissue-417sender fixerseq nreceive_jobsp3reviewer

The kernel records the sender it admitted

The fixer's payload names tester, but the send schema carries only message_key and payload. The kernel resolves the caller from its credential and stores issue-417 with sender fixer, which is what the reviewer reads. Mailboxes.

One sender-attesting host with three processes and the jobs mailbox, following the mailbox comparison. Sequence numbers are drawn symbolically; claims, leases, cursors, and the receipt each call produces are left out.

Attribution is not a signature: the kernel records which admitted process called, and no key signs the message. It names the admitted process, never the truth of the content, so a legitimate producer can still send a wrong fix. Keep business validation and any required approval beside the origin check.

The invoking capability must match exactly one persisted process by capability id, subject key, and content hash, and that process must be running and inside its capability's issued-to-expiry window. Zero matches and two matches produce the same refusal, so a caller cannot tell them apart.

Read more in Mailboxes, which covers how a consumer handles sender records day to day.

A message key belongs to its first payload and sender

MailboxStore::send checks each incoming message_key against any row already stored under it. The same payload hash and sender is a replay: it returns the original sequence, even after acknowledgement, so a retried send writes once.

A different payload hash or a different sender under the same key is refused as a conflict. Knowing another producer's key gives no way to overwrite its message, because the key belongs to whatever first committed it.

Acknowledgement binds no key or sender. Any holder of a channel's ack grant discards every message through the sequence it names, unread ones included; ack advances acknowledged_through, and a receive from a cursor behind that point returns cursor_expired. Holding an ack grant confers no claim ownership and no origin identity.

A null sender stays unknown

Attribution runs only when a registry is attached. A server opened with MailboxServer::open and no call to attest_senders still serves send, receive, and ack, and stores each send with sender: null; so does any message stored before attestation was enabled.

Treat sender: null as missing attribution, never as the current process, the channel's owner, or the last known producer, and route a message that needs a specific origin for explicit reconciliation. Its key stays bound to the null sender it was recorded under, so a producer with an attested sender cannot later resend it.

Claim, complete, and renew always need a caller the kernel can name. A server without a registry refuses all three outright, and an attesting server refuses them, and a send, whenever the invocation context resolves to no admitted process.

Mailboxes serve only under durable admission for every call

MailboxServer::open constructs a server only when the kernel's durable admission mode is DurableAdmissionMode::All and a qualified durable-admission authority is configured. Both checks run before any tool is registered; an embedder that misses either receives ProcessError::Configuration, "mailboxes require durable admission for all calls".

The CLI process host checks first, at load: it rejects a host policy with "host policy requires durable_admission_mode: all and persistent receipts and revocations", so the host fails to start. It then opens each mailbox server and calls attest_senders immediately, so a running chio process serve attributes every send. Neither error is a per-call refusal from a mailbox tool.

The fixer's name survives a misleading payload

The comparison runs once against the three routes configured in the writer and reviewer example, with three separately authenticated ProcessClient instances, a fresh jobs mailbox, and a new private evidence path. The module records each request before invocation, keeps original responses and receipts, and stops on transport uncertainty.

The complete fixer and reviewer comparison
security-host/mailbox_attribution.pypython
"""Exercise one configured, sender-attesting Chio process host.

The owner supplies three separately authenticated ProcessClients. This module
does not mint credentials, start workers, or authenticate a message signature.
Run it once against a fresh jobs mailbox and a new private evidence directory.
"""
import json
import os
from pathlib import Path
from chio_process import ProcessClient, WorkerError


def save(path, text):
    descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
    with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
        stream.write(text)
        stream.flush()
        os.fsync(stream.fileno())
    descriptor = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
    try:
        os.fsync(descriptor)
    finally:
        os.close(descriptor)


def invoke_recorded(client, evidence, name, operation_key, tool, arguments):
    # Freeze the exact request before dispatch. A transport failure is unresolved.
    request = {"operation_key": operation_key, "server_id": "chio-ipc",
               "tool_name": tool, "arguments": arguments}
    save(evidence / f"{name}.request.json", json.dumps(request, sort_keys=True) + "\n")
    try:
        response = client.invoke(operation_key, "chio-ipc", tool, arguments)
    except WorkerError as error:
        save(evidence / f"{name}.unresolved.json", json.dumps({"error": error.code}) + "\n")
        raise  # Retain the original request; never choose a new key to retry it.
    save(evidence / f"{name}.response.json", json.dumps(response, sort_keys=True) + "\n")
    receipt = response["receipt_json"]
    if not isinstance(receipt, str) or "\n" in receipt:
        raise ValueError("Expected one original signed receipt")
    save(evidence / f"{name}.receipts.jsonl", receipt + "\n")
    return response


def completed(response):
    if response["verdict"] != "allow" or response["terminal_state"]["state"] != "completed":
        raise RuntimeError("Original operation requires attention")
    output = response["output"]
    if not output or output["kind"] != "value" or output["value"].get("isError"):
        raise RuntimeError("Expected a completed mailbox value")
    return output["value"]


def exercise_mailbox(writer: ProcessClient, other_writer: ProcessClient,
                     reviewer: ProcessClient, evidence: Path):
    evidence.mkdir(mode=0o700)  # Existing evidence is a conflict, not a reset.
    if writer.inspect()["process_id"] != "fixer" or other_writer.inspect()["process_id"] != "tester":
        raise ValueError("Wrong independently supplied writer routes")
    if reviewer.inspect()["process_id"] != "reviewer":
        raise ValueError("Wrong independently supplied reviewer route")
    payload = {"fix": "issue-417", "declared_sender": "tester"}
    arguments = {"message_key": "issue-417", "payload": payload}
    original = invoke_recorded(writer, evidence, "original", "issue-417/send/v1", "send_jobs", arguments)
    sequence = completed(original)["sequence"]
    retry = invoke_recorded(writer, evidence, "retry", "issue-417/send/v1", "send_jobs", arguments)
    if retry != original:
        raise RuntimeError("Exact retry did not return the original response and receipt")
    resend = invoke_recorded(writer, evidence, "resend", "issue-417/resend/v1", "send_jobs", arguments)
    if completed(resend)["sequence"] != sequence:
        raise RuntimeError("Same message key created another sequence")
    received = invoke_recorded(reviewer, evidence, "receive", "issue-417/receive/v1",
                               "receive_jobs", {"after_sequence": "0", "limit": 10})
    messages = completed(received)["messages"]
    if len(messages) != 1 or messages[0]["sender"] != "fixer" or messages[0]["payload"] != payload:
        raise RuntimeError("Payload label replaced kernel attribution, or another effect appeared")
    changed = invoke_recorded(writer, evidence, "changed", "issue-417/changed/v1", "send_jobs",
                              {"message_key": "issue-417", "payload": {"fix": "issue-418"}})
    impersonated = invoke_recorded(other_writer, evidence, "other-sender", "issue-417/other/v1",
                                   "send_jobs", arguments)
    conflict = "tool server error: mailbox message key or claim conflicts with the recorded payload, sender or claimant"
    for response in (changed, impersonated):
        if response["verdict"] != "deny" or response["output"] is not None or response["reason"] != conflict:
            raise RuntimeError("Expected the exact signed mailbox conflict")
    ack = invoke_recorded(reviewer, evidence, "ack", "issue-417/ack/v1", "ack_jobs",
                         {"through_sequence": sequence})
    if completed(ack)["status"] != "acknowledged":
        raise RuntimeError("Acknowledgment did not complete")
    after_ack = invoke_recorded(writer, evidence, "after-ack", "issue-417/after-ack/v1", "send_jobs", arguments)
    if completed(after_ack)["sequence"] != sequence:
        raise RuntimeError("Acknowledgment forgot the original message binding")
    expired = invoke_recorded(reviewer, evidence, "old-cursor", "issue-417/old-cursor/v1",
                              "receive_jobs", {"after_sequence": "0", "limit": 10})
    if completed(expired) != {"status": "cursor_expired", "acknowledged_through": sequence}:
        raise RuntimeError("Expected the actual acknowledged cursor boundary")
    empty = invoke_recorded(reviewer, evidence, "empty", "issue-417/empty/v1",
                            "receive_jobs", {"after_sequence": sequence, "limit": 10})
    if completed(empty)["messages"]:
        raise RuntimeError("Resend after acknowledgment recreated consumed work")
    print("ATTRIBUTE fixer: payload's declared_sender cannot select the kernel sender")
    print("REPLAY original response; exact resend keeps the original sequence")
    print("REFUSE changed payload and other sender with signed mailbox conflicts")
    print("ACK original sequence; later exact resend creates no visible message")
    print("CURSOR before the acknowledged sequence expires; continue from the retained sequence")
    return original
Executed mailbox comparisontext
ATTRIBUTE fixer: payload's declared_sender cannot select the kernel sender
REPLAY original response; exact resend keeps the original sequence
REFUSE changed payload and other sender with signed mailbox conflicts
ACK original sequence; later exact resend creates no visible message
CURSOR before the acknowledged sequence expires; continue from the retained sequence

You should see five lines. The reviewer reads sender fixer although the payload's declared_sender says tester. An exact retry under the same operation key returns the original response and receipt, while a resend under a new operation key is a new invocation that still keeps the original sequence. The changed payload and the other sender both end in the signed conflict "tool server error: mailbox message key or claim conflicts with the recorded payload, sender or claimant", and after the ack a resend creates no visible message.

Verify the original receipt against trusted-kernel.pub from the independently trusted host installation. The verifier takes that file path, never a key read from the receipt under check.

Terminalbash
chio receipt verify \
  --input ./mailbox-evidence/original.receipts.jsonl \
  --trusted-kernel-pubkey ./trusted-kernel.pub \
  --json

A successful report checks the signature, the signer pin, and the action-parameter hash. Run the same check for the retained changed-payload and other-sender denials, and keep the original receipt bytes exactly as stored. The offline check covers the receipt alone; log completeness and fresh policy authority are separate questions, and the live comparison is what observed the delivered sender and payload.

Unattributed and conflicting calls end in a signed denial

Through the kernel, a mailbox refusal reaches the process as a signed Deny with a receipt. A trusted host that calls the connection directly, without the kernel, receives KernelError::ToolServerError instead, so match the path the caller used.

RefusalCause
Signed Deny, reason "tool server error: mailbox caller is not one live attested process"
A send, claim, complete, or renew resolved to no live admitted process, or to more than one
Signed Deny, reason "tool server error: mailbox message key or claim conflicts with the recorded payload, sender or claimant"
A key replayed with another payload or sender, or a claim, completion, or renewal that no longer matches the recorded holder

For an unattributed caller, reconcile the process identity or the recorded claim holder; a process that is no longer running is refused the same way. For a conflict, a changed payload or another sender needs a reviewed new message key, while a lost transport response needs the original operation identity and unchanged arguments.

Source reference: dispatch, registry matching, and error wrapping
  • Attribution. MailboxServer::attest_senders (mod.rs:84) attaches the registry; dispatch (mod.rs:146-161) resolves the caller for send, claim, complete, and renew.
  • Matching. ProcessRegistry::caller (registry.rs:100) and its free function (children.rs:258-289) keep the one process whose stored capability shares the call's capability id, subject key, and content hash.
  • Key binding. MailboxStore::send (store.rs:75-108) replays a matching row and refuses a differing one.
  • Durable admission. MailboxServer::open (mod.rs:44-56) returns ProcessError::Configuration (types.rs:10); the CLI host's policy check (state.rs:430-435) and its attest_senders call (serving.rs:124-134) run at load.
  • Attested claims. A server with no registry refuses claim, complete, and renew (mod.rs:159). The tests claims_require_an_attested_caller (mailboxes.rs:1134), attesting_servers_refuse_sends_without_a_kernel_caller (mailboxes.rs:892), and attesting_hosts_record_the_kernel_selected_sender_and_bind_keys_to_it (mailboxes.rs:856) exercise both servers.
  • Error wrapping. MailboxServer::invoke and invoke_with_context convert every ProcessError through tool_error (mod.rs:259-271) into KernelError::ToolServerError. The kernel records a Deny receipt and returns Ok(ToolCallResponse { verdict: Deny, reason: Some("tool server error: <reason>"), receipt, .. }) (async_evaluation_core.rs:1873-1898), so ProcessError::Unauthenticated, Cancelled, and ProcessError::Conflict (types.rs:8,18) never reach a process directly.

Next: place attribution among the other shared boundaries