How trust scores work
Every inbound message from an external sender carries amessageTrustScore between 0.0 and 1.0. Curia computes this score in the dispatch layer before the coordinator ever sees the message.
The score combines three inputs:
| Channel | Trust level | Normalized weight |
|---|---|---|
| CLI | High | 1.0 |
| Signal | High | 1.0 |
| HTTP API | Medium | 0.6 |
| Low | 0.3 |
| Trust level | Who gets it |
|---|---|
low | Default for newly confirmed contacts |
medium | Contacts with moderate interaction history |
high | Contacts you’ve explicitly trusted — EA, CFO, board members |
ceo | Reserved for the CEO. Set automatically at startup via the bootstrap process — cannot be assigned through contact-set-trust. |
ceo trust level outranks all others. It bypasses PII redaction, outbound content filters, and trust score thresholds. The meetsMinimumTrust() check uses ordinal comparison, so ceo satisfies any gate that requires high, medium, or low.
Contact confidence accumulates over time as signal builds. A brand-new email sender starts at zero. A CEO-verified Signal contact with a long interaction history may carry 0.95.
Contact confidence is computed automatically by the confidence scoring pipeline — it updates on every qualifying event rather than being set manually:
| Event | Effect |
|---|---|
| Inbound message received | Incremental increase based on message history |
| Outbound reply sent | Incremental increase (bidirectional contact) |
| CEO explicit trust grant | Larger positive boost |
| Verified identity pairing confirmed | Positive boost |
contact_confidence directly — it is always derived from observable history.
Content risk reduces the score when the dispatch layer detects injection-like patterns in the message body. Clean messages incur no penalty.
Score examples
| Sender | Score | Why |
|---|---|---|
| Brand-new email sender | ~0.12 | Low channel trust (0.3 × 0.4) + zero contact confidence |
| CEO-verified Signal contact | ~0.80 | High channel trust (1.0 × 0.4) + high confidence (0.95 × 0.4) |
| Unknown Signal sender | ~0.40 | High channel trust + zero contact confidence |
| Known email contact (long history) | ~0.52 | Low channel trust + high confidence |
Action thresholds
The coordinator checks the sender’smessageTrustScore before taking action. If the score falls below the threshold for the requested action, the request is declined.
| Action category | Minimum score | What this means |
|---|---|---|
| Information queries | 0.2 | Any authenticated message qualifies |
| Scheduling | 0.5 | Medium trust channel or a known email contact |
| Data export | 0.8 | High-trust channel and a verified contact |
| Financial actions | 0.8 | High-trust channel and a verified contact |
config/default.yaml under security.trust_thresholds and can be adjusted without a code change. Startup fails if this block is missing or malformed.
Trust thresholds do not apply to you (the CEO). Messages arriving via CLI, or from a contact with the
ceo role, are always trusted regardless of score. Curia never explains the trust system or mentions scores to external senders.Email sender verification
Email is the highest-risk channel because From headers can be spoofed. Curia adds an extra layer on top of the trust score: SPF, DKIM, and DMARC validation. Your email provider performs these checks at the server level. The email channel adapter reads theAuthentication-Results headers on every inbound message and maps them to a senderVerified flag:
senderVerified: true— SPF, DKIM, and DMARC all passsenderVerified: false— any check fails, or headers are absent (fails closed)
senderVerified: false, the coordinator will not take consequential actions — financial, data, or access changes — without confirmation via a verified channel like Signal or CLI.
Handling unknown senders
When Curia receives a message from someone not in your contacts, it follows the hold and notify policy for email and Signal: the message is held in a queue, and Curia mentions it to you at the next opportunity.Message arrives from an unknown sender
The dispatch layer looks up the sender’s channel identifier (
email address, Signal number) in the contacts database. No match is found. The message is placed in the held messages queue. An audit event is written with the sender identifier, channel, and routing decision.Curia notifies you
At the next conversation, Curia mentions the oldest held message: “By the way, you have a held email from
[email protected] about ‘Q3 Numbers’. Want me to identify them?”Curia surfaces one held message at a time — not a list — to avoid interrupting the conversation flow. You can ask for the full list at any time.You decide what to do
You have three options for each held message:
- Identify — tell Curia who the sender is. Curia creates a confirmed contact and replays the held message through normal processing with full contact context.
- Dismiss — discard the message. The sender’s provisional contact record is preserved in case they reach out again.
- Block — discard the message and mark the sender as blocked. Future messages from this sender are dropped without acknowledgment.
Automatic contact creation from email
When Curia processes an inbound email, it auto-creates provisional contacts for every participant in the From, To, and CC headers that isn’t already in your contacts. This builds your contact database naturally as emails flow through. To protect against spam campaigns that flood your contacts with thousands of junk entries, auto-creation is rate-limited:| Limit | Default | What happens when hit |
|---|---|---|
| Per message | 10 | Remaining participants in that email are skipped |
| Per hour | 100 per email account | All further auto-creation for that account pauses until the window resets |
config/local.yaml:
Existing contacts are not counted toward the limits — only genuinely new contact creations. If a 20-person thread has 18 known contacts, only the 2 new ones count.
Automatic promotion of provisional contacts
Provisional contacts are automatically promoted to confirmed when Curia detects evidence of a real relationship. Three signals trigger promotion:- Curia outbound — Curia has sent an outbound message to the contact’s email address (any account).
- CEO sent email — the CEO has sent them an email directly (detected via a daily Sent folder scan).
- Calendar acceptance — the CEO has accepted a calendar event where the contact is an attendee.
Auto-promotion only applies to provisional contacts whose email was established through a trusted source (email participant headers, calendar attendees). It never applies to self-claimed channel identities — those always require explicit CEO confirmation.
Adding contacts proactively
You don’t have to wait for someone to reach out. Tell Curia about people directly in conversation:“Add Sarah Chen, CFO at Acme. Her email is [email protected] and she’s on Signal at +15550001111.”Curia creates a contact record, links both channel identities as verified, and assigns the CFO role with its default permissions. You can also update existing contacts:
“Jenna’s new work email is [email protected].”
“Grant Jenna permission to send emails on my behalf.”All contact mutations are confirmed with you before they’re written, and every change is audit-logged.
Contact profile attributes
Beyond identity and trust, Curia keeps a small set of canonical profile attributes directly on each contact record: preferred name, job title, organization, primary email and phone, timezone, locale, location, pronouns, LinkedIn URL, a short bio, and birthday. These are stored as structured fields on the contact itself — not as free-floating knowledge-graph facts — so agents read a reliable value instead of inferring one from a list of remembered notes. That distinction matters: it’s what stops Curia from guessing a stale address or timezone when composing on your behalf. You set them the same conversational way as anything else (“Sarah’s title is now VP of Sales”, “her timezone is US Pacific”), and Curia writes them via thecontact-update skill (phone numbers are normalized to E.164). Richer, narrative knowledge about a person — relationships, history, preferences — still lives in the knowledge graph; the canonical attributes are just the handful of stable fields worth pinning down. The console’s contact view exposes the same fields for direct editing.
Contact deduplication
Curia runs a weekly background scan for likely duplicate contacts — for example, “Jenna Torres” and “J. Torres” who share the same email address. When potential duplicates are detected, Curia presents them to you for review:“I noticed two contacts that look like the same person:Curia never auto-merges contacts. You confirm every merge.I’d merge them into Jenna Torres (CFO). Want me to proceed?”
- Jenna Torres (CFO, verified email [email protected])
- J. Torres (no role, email [email protected])
How authorization works
Authorization runs through a three-layer check on every request from a non-CEO sender:- Per-contact overrides — explicit grants and denials you’ve set for this specific contact. These always win over role defaults.
- Role defaults — the default permissions and denials for the contact’s role (CFO, board member, direct report, advisor, etc.).
- Channel trust gate — even if role and overrides permit an action, the channel must be trusted enough. Financial and data actions require a high-trust channel.
Allowed, Denied, Blocked by channel trust, or Needs CEO decision.
Channel identity status
Every channel identity (email address, phone number, etc.) linked to a contact carries astatus field, separate from whether it has been verified:
| Status | Meaning |
|---|---|
active | The identity is current and preferred for outbound use |
defunct | The identity is no longer valid (e.g., old email address) |
bounced | Outbound delivery to this address has failed |
active identities when assembling outbound context. When non-active identities would be used — for example, if a contact’s only linked email is defunct — the Contact Specialist warns the Coordinator before suggesting them. You can update an identity’s status directly:
“Mark sarah’s old acme.com email as defunct — she’s at beacon.com now.”Curia uses the
contact-set-identity-status skill. Status is orthogonal to verification: a verified identity can be defunct, and an active identity may be self_claimed until you confirm it.
Self-claimed identities
If someone messages on a new channel and claims to be an existing contact (“Hi, it’s Jenna”), that identity is taggedself_claimed and treated as unverified until you confirm it. Curia never auto-promotes self-claimed identities to verified status. An unverified identity is gated the same as an unknown sender for any consequential action.
Contact statuses at a glance
| Status | Who | Permissions |
|---|---|---|
| Provisional | New contacts — not yet confirmed by you | None. Curia acknowledges but takes no action. |
| Confirmed | Contacts you’ve verified or that arrived from authoritative sources (CRM, calendar) | Role defaults + any per-contact overrides |
| Blocked | Contacts you’ve blocked | None. Messages are dropped without response. |