@joe-broadhead/messaging 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +259 -2
- package/dist/bin/messaging-mcp.js +5484 -3858
- package/dist/bin/messaging-relay.js +4728 -1136
- package/dist/bin/messaging.js +4616 -2807
- package/dist/bin/messagingd.js +16451 -9444
- package/dist/lib/modules/core/agent/operations.js +19 -327
- package/dist/lib/modules/core/agent/present.js +15 -55
- package/dist/lib/modules/core/agent-tools.js +46 -143
- package/dist/lib/modules/core/consent.js +1 -1
- package/dist/lib/modules/core/conversation/context.js +80 -0
- package/dist/lib/modules/core/conversation/event.js +125 -0
- package/dist/lib/modules/core/conversation/guest-invitation.js +42 -0
- package/dist/lib/modules/core/conversation/ids.js +38 -0
- package/dist/lib/modules/core/conversation/link.js +35 -0
- package/dist/lib/modules/core/conversation/log.js +988 -0
- package/dist/lib/modules/core/conversation/ratchet.js +243 -0
- package/dist/lib/modules/core/conversation/request-log.js +158 -0
- package/dist/lib/modules/core/conversation/request-seal.js +41 -0
- package/dist/lib/modules/core/conversation/requests.js +87 -0
- package/dist/lib/modules/core/conversation/seal.js +251 -0
- package/dist/lib/modules/core/conversation-agent-tools.js +69 -0
- package/dist/lib/modules/core/federation/directory-proof.js +83 -0
- package/dist/lib/modules/core/federation/proof-state.js +167 -0
- package/dist/lib/modules/core/federation/roster-proof.js +74 -0
- package/dist/lib/modules/core/index.js +16 -2
- package/dist/lib/modules/core/jelly-host.js +5 -5
- package/dist/lib/modules/core/schema/common.js +85 -3
- package/dist/lib/modules/core/schema/consent.js +5 -4
- package/dist/lib/modules/core/schema/contact-request.js +22 -0
- package/dist/lib/modules/core/schema/conv-frames.js +94 -0
- package/dist/lib/modules/core/schema/conversation.js +326 -0
- package/dist/lib/modules/core/schema/envelope.js +8 -24
- package/dist/lib/modules/core/schema/errors.js +42 -2
- package/dist/lib/modules/core/schema/guest-invitation.js +13 -0
- package/dist/lib/modules/core/schema/index.js +6 -0
- package/dist/lib/modules/core/schema/ipc/consent.js +13 -9
- package/dist/lib/modules/core/schema/ipc/conversation-views.js +80 -0
- package/dist/lib/modules/core/schema/ipc/conversations.js +128 -0
- package/dist/lib/modules/core/schema/ipc/device.js +2 -0
- package/dist/lib/modules/core/schema/ipc/jelly.js +3 -38
- package/dist/lib/modules/core/schema/ipc/shapes.js +14 -72
- package/dist/lib/modules/core/schema/ipc/surfaces.js +12 -195
- package/dist/lib/modules/core/schema/ipc/tasks.js +31 -0
- package/dist/lib/modules/core/schema/ipc.js +18 -44
- package/dist/lib/modules/core/testing.js +1 -0
- package/dist/lib/modules/core/version.js +1 -1
- package/dist/lib/modules/ipc/client.js +10 -2
- package/dist/lib/modules/ipc/server.js +14 -5
- package/dist/lib/modules/ipc/testing.js +13 -23
- package/dist/spec/messaging-v1.md +835 -380
- package/dist/third-party-notices.md +1 -1
- package/dist/third-party.json +1 -1
- package/dist/types/core/agent/operations.d.ts +5 -52
- package/dist/types/core/agent/present.d.ts +6 -21
- package/dist/types/core/agent-tools.d.ts +277 -438
- package/dist/types/core/conversation/context.d.ts +56 -0
- package/dist/types/core/conversation/event.d.ts +36 -0
- package/dist/types/core/conversation/guest-invitation.d.ts +8 -0
- package/dist/types/core/conversation/ids.d.ts +18 -0
- package/dist/types/core/conversation/link.d.ts +16 -0
- package/dist/types/core/conversation/log.d.ts +157 -0
- package/dist/types/core/conversation/ratchet.d.ts +95 -0
- package/dist/types/core/conversation/request-log.d.ts +24 -0
- package/dist/types/core/conversation/request-seal.d.ts +6 -0
- package/dist/types/core/conversation/requests.d.ts +14 -0
- package/dist/types/core/conversation/seal.d.ts +70 -0
- package/dist/types/core/conversation-agent-tools.d.ts +918 -0
- package/dist/types/core/federation/directory-proof.d.ts +8 -0
- package/dist/types/core/federation/proof-state.d.ts +114 -0
- package/dist/types/core/federation/roster-proof.d.ts +7 -0
- package/dist/types/core/index.d.ts +15 -3
- package/dist/types/core/jelly-host.d.ts +1 -1
- package/dist/types/core/schema/common.d.ts +85 -4
- package/dist/types/core/schema/consent.d.ts +7 -7
- package/dist/types/core/schema/contact-request.d.ts +264 -0
- package/dist/types/core/schema/conv-frames.d.ts +1200 -0
- package/dist/types/core/schema/conversation.d.ts +2803 -0
- package/dist/types/core/schema/envelope.d.ts +89 -130
- package/dist/types/core/schema/errors.d.ts +40 -0
- package/dist/types/core/schema/guest-invitation.d.ts +51 -0
- package/dist/types/core/schema/index.d.ts +6 -0
- package/dist/types/core/schema/ipc/consent.d.ts +89 -49
- package/dist/types/core/schema/ipc/conversation-views.d.ts +1023 -0
- package/dist/types/core/schema/ipc/conversations.d.ts +3987 -0
- package/dist/types/core/schema/ipc/device.d.ts +10 -0
- package/dist/types/core/schema/ipc/jelly.d.ts +2 -105
- package/dist/types/core/schema/ipc/shapes.d.ts +16 -95
- package/dist/types/core/schema/ipc/surfaces.d.ts +28 -424
- package/dist/types/core/schema/ipc/tasks.d.ts +208 -0
- package/dist/types/core/schema/ipc.d.ts +2684 -523
- package/dist/types/core/schema/relay.d.ts +20 -4
- package/dist/types/core/testing.d.ts +1 -0
- package/dist/types/core/version.d.ts +1 -1
- package/dist/types/ipc/client.d.ts +4 -1
- package/dist/types/ipc/server.d.ts +10 -1
- package/dist/types/ipc/testing.d.ts +21 -8
- package/package.json +1 -1
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# Messaging protocol v1 (`messaging/1`)
|
|
2
2
|
|
|
3
|
+
## Conversation IPC cut-over (2026-11-10)
|
|
4
|
+
|
|
5
|
+
The live local API uses `CONVERSATION_METHODS` and `CONVERSATION_TIERS` from the normative core schema.
|
|
6
|
+
The old `msg.send`, `person.send`, `channel.*`, `search`, `conv.subscribe`, `agents.list` and `agents.read`
|
|
7
|
+
methods are removed, without aliases. `conv.list`, `conv.read` and batch `people.get`
|
|
8
|
+
use the conversation schemas. Agent access follows design §8.5: `conv.get`, `conv.members` and `file.get`
|
|
9
|
+
are human-only contracts. `file.get` remains staged. `sessions.list` is human-only and returns local sessions and their conversation seats.
|
|
10
|
+
`request.waiting` labels an authenticated outgoing hold `held`, and missing-receipt evidence
|
|
11
|
+
`pending-delivery`; missing receipts do not establish consent state. Contact requests remain visible while delivery is unconfirmed.
|
|
12
|
+
Only a verified recipient receipt changes a contact request to `held`. Internal change notifications carry
|
|
13
|
+
identifiers only; `conv.sync` re-reads verified state and selects safe rendered fields. Storage tables remain.
|
|
14
|
+
A line first readable after its key arrives streams as an `event`; `event.update` carries only edits, deletions, reactions, pins and delivery.
|
|
15
|
+
|
|
16
|
+
The daemon binds every agent call to a session registered on that connection. It rechecks membership and
|
|
17
|
+
session ownership after asynchronous rendering. Agents edit/delete only their own lines. Cursors bind to
|
|
18
|
+
the query and session; a daemon restart or engine replacement returns a stream gap. Replay renders current
|
|
19
|
+
verified state, including edits and deletions, rather than caching historical plaintext. Projection refuses
|
|
20
|
+
prefixes above 10,000 entries and responses above 512 KiB. These are interim safety bounds pending load tests.
|
|
21
|
+
Only `LIVE_CONVERSATION_METHODS` is served. Later-lane contracts stay staged and have no socket handler.
|
|
22
|
+
`contacts.list` returns verified conversation metadata: `cid`, `kind`, optional `name`, and optional `dmWith`.
|
|
23
|
+
Only accepted conversations appear. Contacts carry no transcript, read position, or unread count.
|
|
24
|
+
Channel contacts use the canonical `cid` as their address. Agent clients filter them through `conv.access`.
|
|
25
|
+
Shared core operations retain contacts, inbox reads, and tasks. Conversation clients use `conv.*` for sends, pages, and search.
|
|
26
|
+
Lane 4 omits read positions, preferences, unread counts and thread-follow state from views until lane 6 implements them.
|
|
27
|
+
Filters on that state (`conv.list` `unread` and `mentions`, `conv.threads` `unread` and `following`) are refused with `invalid_request`, never answered with an empty page.
|
|
28
|
+
`conv.backfill` verifies available relay history and returns `complete: false`. Completion requires an indexing coverage boundary that accounts for missing keys and expired entries.
|
|
29
|
+
`clientId` deduplication commits atomically with a queued line and survives engine replacement; reuse with
|
|
30
|
+
different content or a different destination is refused. Keyless conversation calls on a Jelly admin socket
|
|
31
|
+
require an action grant, even when the same method is available to a bound agent session.
|
|
32
|
+
|
|
33
|
+
Conversation maintenance coalesces verified membership changes for a random 0–30 second delay before answering
|
|
34
|
+
shared channel history. Only accepted participants qualify. Successful responses are remembered per membership,
|
|
35
|
+
epoch and recipient device set; failed or keyless responses remain retryable. The daemon sweep retries the
|
|
36
|
+
conversation outbox, schedules due epoch rekeys, and expires local message ciphertext/plaintext and search rows
|
|
37
|
+
while preserving digest links and control operations. Shutdown cancels response timers and waits for active work.
|
|
38
|
+
|
|
39
|
+
Tasks and delegation retain their envelopes and lifecycle until lane 7. Agent-tier `task.create` replaces
|
|
40
|
+
the task-creation arm of removed `msg.send`: it takes `key`, an agent `to`, `parts`, optional A2A fields,
|
|
41
|
+
`wake`, `idempotencyKey`, and `onBehalfOf`. It assigns the task ID and signs the existing delegation chain.
|
|
42
|
+
Agent-tier `task.continue` takes the same message fields and a required `taskId`, without `onBehalfOf`.
|
|
43
|
+
It sends the retained task-follow-up envelope only to that session's existing task executor, preserving
|
|
44
|
+
context, request binding, receipts, and terminal-state checks. Both return `{results: SendResult[]}`.
|
|
45
|
+
`task.list/get/update/cancel` are unchanged. Plain mail uses `conv.send`, never a task envelope.
|
|
46
|
+
The daemon rejects plain agent-mail envelopes, including previously held mail released after cut-over;
|
|
47
|
+
it retains the database rows but does not expose them through inbox or item reads.
|
|
48
|
+
|
|
49
|
+
The inbox combines retained task requests/follow-ups with verified incoming lines in accepted two-agent DMs
|
|
50
|
+
addressed to this local session. Plain legacy mail rows are not returned. For a line, `id`, `messageId` and
|
|
51
|
+
`eventId` equal the event ID, and `cid` names the conversation. `from.address` is the stable participant ID;
|
|
52
|
+
its device is the verified signing device, and its label is absent, never synthesized. Task items retain their device provenance.
|
|
53
|
+
`inbox.get/ack` and `item.read` accept an optional `cid`; an ambiguous ID requires it. The full `message`
|
|
54
|
+
projection contains the line text, while `cid/eventId` lets the client read the safe event and file references.
|
|
55
|
+
Edits are rendered on every read; deleted, inaccessible and unaccepted lines are not returned. `receivedAt`
|
|
56
|
+
is the first local observation; `bytes` counts UTF-8 text. Stages are durable, monotonic local acknowledgments.
|
|
57
|
+
Acknowledgments belong to the stable recipient participant and `(cid, eventId)`, not a process session key.
|
|
58
|
+
An agent restart with a new session key preserves these stages and does not repeat wake hints for acknowledged lines.
|
|
59
|
+
They do not manufacture envelope receipts. Recipient `ask` governs agent-DM admission: `open` accepts, `request` holds, and `no` silently declines.
|
|
60
|
+
After admission, lines in the accepted agent DM follow the answer path. A later person block suppresses them.
|
|
61
|
+
Person blocks also hide that person's events from conversation reads, search, previews and replay.
|
|
62
|
+
The verified log remains intact. Hidden lines do not contribute reply summaries or reactions to visible lines.
|
|
63
|
+
The push tier governs wake hints. New eligible lines use the existing session event and wake bridge; acknowledgments do
|
|
64
|
+
not re-notify. Agent reads carry the same audience-label effect as conversation reads.
|
|
65
|
+
|
|
3
66
|
Status: v1. The Zod schemas in `packages/core/src/schema/` are the
|
|
4
67
|
normative wire contract; this document gives the semantics. If the two disagree, fix one of them in
|
|
5
68
|
the same change, and add a test vector.
|
|
@@ -16,8 +79,8 @@ other, exchange messages, and delegate A2A tasks, through a relay that only sees
|
|
|
16
79
|
(`urn:messaging:binding:relay:v1`), presence and session addressing, delivery receipts, and the
|
|
17
80
|
extensions `urn:messaging:ext:provenance:v1` and `urn:messaging:ext:presence:v1`.
|
|
18
81
|
|
|
19
|
-
Not in v1: hole-punched peer-to-peer transport, federation between relays, payments
|
|
20
|
-
(
|
|
82
|
+
Not in v1: hole-punched peer-to-peer transport, federation between relays, and payments. Conversations
|
|
83
|
+
(§16) encrypt to many members with sender keys; MLS is left to a later protocol version.
|
|
21
84
|
|
|
22
85
|
## 2. Encodings
|
|
23
86
|
|
|
@@ -54,8 +117,12 @@ Not in v1: hole-punched peer-to-peer transport, federation between relays, payme
|
|
|
54
117
|
`messaging/1/invite\n`, `messaging/1/invite-join\n`, `messaging/1/delegation\n`, `messaging/1/grant\n`,
|
|
55
118
|
`messaging/1/card\n`, and for hosted realms (§15) the person hash prefix
|
|
56
119
|
`messaging/1/person\n`, `messaging/1/dir-read\n`, `messaging/1/device-log\n`, `messaging/1/device-keys\n`,
|
|
57
|
-
`messaging/1/device-link\n`, `messaging/1/epoch\n` and `messaging/1/dir-key\n
|
|
58
|
-
|
|
120
|
+
`messaging/1/device-link\n`, `messaging/1/epoch\n` and `messaging/1/dir-key\n`, and for conversations (§16) the
|
|
121
|
+
event signature prefix `messaging/1/conv-event\n` and the hash prefixes `messaging/1/conv-genesis\n`,
|
|
122
|
+
`messaging/1/dm\n` and `messaging/1/self\n`, and for contact requests and guest invitations
|
|
123
|
+
`messaging/1/contact-request\n`, `messaging/1/contact-request-content\n`, `messaging/1/contact-request-digest\n`,
|
|
124
|
+
`messaging/1/contact-request-decision\n`, `messaging/1/contact-authorization\n` and `messaging/1/guest-invitation\n`).
|
|
125
|
+
A signature or hash for one purpose never verifies for another.
|
|
59
126
|
- **Hash:** SHA-256, base64url.
|
|
60
127
|
|
|
61
128
|
## 3. Identity
|
|
@@ -474,7 +541,8 @@ Only the executor changes state (see `canTransition` in core).
|
|
|
474
541
|
still run, and the person is away from that device. The host says so with `person.attached`; a Jelly host's device is
|
|
475
542
|
away until its first `person.attached {attached: true}`, and no other device ever says `away`. A receiver shows that
|
|
476
543
|
device as away (`people.get`), and an `away` older than the online window (§15.9: 150 s) means nothing.
|
|
477
|
-
- `features` advertises optional capabilities. v1 defines `"tasks"
|
|
544
|
+
- `features` advertises optional capabilities. v1 defines `"tasks"` and `"conversations"` (the daemon reads conversation
|
|
545
|
+
logs; `conv.caps` reads it at the relay). A receiver ignores a feature it does not know.
|
|
478
546
|
- `label` must equal the sender's roster label. Receivers ignore a presence whose label differs, and
|
|
479
547
|
always show the roster's label.
|
|
480
548
|
- Like every envelope, presence carries the sender's `anchor` in its header (§5). Presence is
|
|
@@ -540,7 +608,7 @@ forwards a credential it was handed.
|
|
|
540
608
|
- `exp`, the earliest along the chain.
|
|
541
609
|
|
|
542
610
|
The requester's record holds the chain it sent.
|
|
543
|
-
- **Delegating (§8.2).** A session delegates with `
|
|
611
|
+
- **Delegating (§8.2).** A session delegates with `task.create {onBehalfOf}`. Only
|
|
544
612
|
the sending daemon signs, and clients never see or build grants.
|
|
545
613
|
- Without `onBehalfOf`, a task starts a new chain whose root is the sending session.
|
|
546
614
|
- A root grant made over IPC is unrestricted: no `scope`, and `maxDepth = LIMITS.delegationDepth`.
|
|
@@ -574,107 +642,26 @@ forwards a credential it was handed.
|
|
|
574
642
|
code: fenced in a model context, `terminalSafe` in a terminal, `cleanPeerText` in a permission
|
|
575
643
|
prompt. A client shows a chain only when it adds something beyond `from`: depth > 1 or a scope.
|
|
576
644
|
|
|
577
|
-
### 6.8
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
always the channel's own name.
|
|
593
|
-
- A DM line: one copy to each machine of the person, and none to the sender's own machines.
|
|
594
|
-
- A DM to oneself (`dm:<own person>`, the person's own conversation, "me"): one copy to each of the
|
|
595
|
-
person's other machines. A line one of the person's agents wrote there is for the person, so every
|
|
596
|
-
machine files it as incoming, unread; a line the person wrote is outgoing. Its lines carry no
|
|
597
|
-
delivery `state`: there is no other person to deliver them to.
|
|
598
|
-
- `create {id, name, persons, creator}`: the full record, one copy to each member machine but the
|
|
599
|
-
sender's. `persons` includes the sender; `creator` is the person who created the channel, so an
|
|
600
|
-
invitee learns it from the inviter's `create`.
|
|
601
|
-
- `invite {id, name, persons}`: the persons added, to the machines of the existing members. Each
|
|
602
|
-
machine of an added person gets a `create` with the full record instead. Persons with no
|
|
603
|
-
machine left on the roster are not in that record.
|
|
604
|
-
- `leave {id, name}`: the sender left, to every member machine.
|
|
605
|
-
- `remove {id, name, persons}`: the persons removed, to every member machine, the removed
|
|
606
|
-
persons' included.
|
|
607
|
-
- `post {id, name}`: a line, to every member machine.
|
|
608
|
-
- The text of a channel op is not filed. Each machine files its own notice line for an op it
|
|
609
|
-
applies, written by the daemon from the op's fields, with the op's `messageId` (§8.2 `notice`).
|
|
610
|
-
- **Receiver rules.** Channels are looked up by `id`, never by name, and an op's `name` must match
|
|
611
|
-
the channel's own name (`not_found`). Where a rule fails, nothing is stored and the sender gets a
|
|
612
|
-
`rejected` receipt.
|
|
613
|
-
- `create` is accepted only for an unknown `id`, only when the sender and this machine's person are
|
|
614
|
-
both in `persons` (`forbidden`), and not for an id this machine
|
|
615
|
-
left or was removed from within the last `LIMITS.ttlMs + LIMITS.clockSkewMs` (`forbidden`). A
|
|
616
|
-
machine holds at most a policy number of channels (the reference daemon: 200); a `create`
|
|
617
|
-
beyond it is `rate_limited`. If this machine already holds a channel with that name, it keeps
|
|
618
|
-
the new one under a local name: the name cut to 28 characters, a hyphen and the lowest free
|
|
619
|
-
number from 2 (`eng-2`), or `rate_limited` when 2 to 99 are all taken. Room references
|
|
620
|
-
(`channel:<name>`), `conv.*`, `search`, `person.send` and `channel.*` use the local name; envelopes always carry the
|
|
621
|
-
channel's own name. This also covers two people creating the same name at once.
|
|
622
|
-
- A `create` or `invite` naming a person this machine cannot admit is `not_found`: in a roster realm a
|
|
623
|
-
person with no current machine; in a directory realm any id that is not a hosted person. A directory
|
|
624
|
-
home takes persons it has not verified yet, then looks them up and watches them (§15.9).
|
|
625
|
-
- `invite` is accepted only from a current member, and only when the channel stays within the
|
|
626
|
-
limit (`forbidden`, `invalid_request`).
|
|
627
|
-
- `leave` removes only the sender. When this machine's person leaves (from another of its
|
|
628
|
-
machines), the channel and its lines are deleted here.
|
|
629
|
-
- `remove` is accepted only from a member who is the channel's creator or a team admin on the
|
|
630
|
-
verified roster (`forbidden`), never naming the sender (`invalid_request`: use `leave`), and only
|
|
631
|
-
for current members (`not_found`). A machine whose person is removed deletes the channel and its
|
|
632
|
-
lines, as on `leave`, and refuses a `create` for that id as above. Other machines remove those
|
|
633
|
-
persons from the channel.
|
|
634
|
-
- `post` is filed only when the sender and this machine's person are both members (`forbidden`).
|
|
635
|
-
- Every name in `persons` must be a person on the roster (`not_found`). All of this is checked
|
|
636
|
-
before anything is stored.
|
|
637
|
-
- A DM line from the receiver's own person is filed in its own conversation, as above.
|
|
638
|
-
- A person who has no machine left on the roster is no longer a member of any channel; each
|
|
639
|
-
machine drops them when it applies the removal.
|
|
640
|
-
- **Delivery.** An accepted chat envelope gets a `delivered` receipt. It creates no inbox item, does
|
|
641
|
-
not count against the inbox quota (§8.3) and never wakes a session. Consent (§6.9) and the inbound
|
|
642
|
-
rate limit still apply. The sender records each chat envelope it routes with its line and
|
|
643
|
-
recipient. A receipt from that recipient's machine, a relay nack, the recipient leaving the roster
|
|
644
|
-
or expiry settles the record, once. An outgoing line is `delivered` once every other person has a
|
|
645
|
-
delivered copy, and `refused` once some person's every copy was rejected or expired; until then
|
|
646
|
-
it is `sent`. When every copy of a channel change to one person is refused, the sender files a
|
|
647
|
-
notice line in that channel naming the person and the code. Notice lines are never sent.
|
|
648
|
-
- **Transcript.** Each daemon keeps one row per line, keyed by `messageId`; a later envelope with a
|
|
649
|
-
`messageId` this machine already holds is not filed and changes nothing. The thread key is the
|
|
650
|
-
first line's `messageId`. Text is the line's text parts joined by newlines, cut to
|
|
651
|
-
`LIMITS.chatChars` UTF-16 code units without splitting a surrogate pair. The sender files its own
|
|
652
|
-
line with the plaintext it sealed; that copy is not on the wire. A person's line that arrives from
|
|
653
|
-
another of their machines is filed as outgoing. Retention deletes lines after
|
|
654
|
-
`retention.transcriptDays` (default 30), and a per-person cap deletes a sender's oldest lines
|
|
655
|
-
first once they hold more than a policy number of lines or bytes (the reference daemon: 10,000
|
|
656
|
-
lines or 16 MiB of UTF-8 text).
|
|
657
|
-
- **Order and read state.** Each daemon numbers its chat changes with one counter that only
|
|
658
|
-
rises and is never reused (`seq`, from 1): every filed line takes the next number, and so does
|
|
659
|
-
every later change of a line (its delivery state) and every channel deleted here. A line keeps the
|
|
660
|
-
number it was filed with as its `seq`, its place in this machine's order; another machine numbers
|
|
661
|
-
the same line differently. Paging and read cursors use it. Read state is per machine and per room:
|
|
662
|
-
a cursor (`readUpTo`, 0 at first) that only moves forward and never past the newest number here.
|
|
663
|
-
A line is unread when another person's line, or the daemon's notice of another person's change,
|
|
664
|
-
was filed after the cursor; this person's own lines never are. Leaving a channel, or being removed
|
|
665
|
-
from it, deletes its cursor with its lines. Nothing syncs read state between machines.
|
|
666
|
-
- **Joining later.** A person invited later does not receive earlier lines. Membership is each
|
|
667
|
-
machine's own view: concurrent invites or removals can leave machines with different member
|
|
668
|
-
lists, and nothing reconciles them. A post refused because of that shows as `refused` to its
|
|
669
|
-
sender.
|
|
670
|
-
- **Reading.** `conv.list`, `conv.read` and `search` (§8.2) see the DMs this machine holds and
|
|
671
|
-
the channels its person is in. Agent-facing clients fence chat text as peer content and ask
|
|
672
|
-
before reading it.
|
|
645
|
+
### 6.8 Conversations: DMs, groups and channels
|
|
646
|
+
|
|
647
|
+
Conversations use the signed log and sender-key encryption in §16. A `cid` identifies a conversation across devices.
|
|
648
|
+
The daemon verifies each prefix before it exposes membership, content or delivery state.
|
|
649
|
+
Legacy chat envelopes and room-name APIs are removed. Existing database tables remain for store compatibility.
|
|
650
|
+
|
|
651
|
+
- **Participants.** A participant is a person or a stable `person/agent` address. Verified bindings identify each signing device.
|
|
652
|
+
- **Admission.** An invitee receives authenticated controls and message digests before acceptance, but no ciphertext or keys.
|
|
653
|
+
Agent-DM admission follows the receiving person's `ask` level (§6.9).
|
|
654
|
+
- **History.** The signed history policy controls access after acceptance. Recovery replaces matching invitation digests with authenticated entries.
|
|
655
|
+
It survives restart and does not restore content deleted by later retention.
|
|
656
|
+
- **Delivery.** Conversation sends return `pending` until verified append evidence arrives. Missing receipts never establish an outgoing consent hold.
|
|
657
|
+
- **Reading.** `conv.list`, `conv.read`, `conv.search` and `conv.context` expose current verified projections.
|
|
658
|
+
Agent reads require a registered session, access checks and the audience guard. Clients fence peer content (§12).
|
|
659
|
+
- **Read state.** Lane 4 omits read positions, unread counts, preferences and follow state. Their methods remain staged until later lanes implement them.
|
|
673
660
|
|
|
674
661
|
### 6.9 Consent
|
|
675
662
|
|
|
676
663
|
Consent belongs to the receiving person. A person's **grants** say what each other person, and the
|
|
677
|
-
|
|
664
|
+
participants of each conversation, may send them and their agents. A device of the person signs each grant,
|
|
678
665
|
only the person's own devices hold it, and the receiving daemon enforces it. Agents never sign or
|
|
679
666
|
change a grant.
|
|
680
667
|
|
|
@@ -693,7 +680,7 @@ A roster realm has only `self` and `org`.
|
|
|
693
680
|
|
|
694
681
|
```ts
|
|
695
682
|
{ v, realm, id: Ulid, person, by: MachineId, ts,
|
|
696
|
-
subject: { person } | {
|
|
683
|
+
subject: { person } | { conv: ConvId },
|
|
697
684
|
chat?: "open" | "muted" | "blocked", // lines from the subject (person subjects only)
|
|
698
685
|
agentMail?: "open" | "blocked", // lines from the subject's agents
|
|
699
686
|
ask?: "open" | "request" | "no", // mail and tasks from the subject or their agents to the person's agents
|
|
@@ -705,8 +692,8 @@ A roster realm has only `self` and `org`.
|
|
|
705
692
|
|
|
706
693
|
- `sig` = Ed25519(`by`, `"messaging/1/grant\n"` ‖ JCS(grant without `sig`)). `by` is a device of `person`.
|
|
707
694
|
- `person` and a person subject are `PersonId`s: a roster person name, or a hosted realm's `p_…` id (§15.1).
|
|
708
|
-
- A
|
|
709
|
-
|
|
695
|
+
- A conversation grant sets only `ask` and `agentMail`. Both persons must be active participants in the locally verified conversation.
|
|
696
|
+
For an event-specific decision, membership is evaluated at that event's position.
|
|
710
697
|
- `trustedUntil` needs `ask: "open"` and is at most `ts + LIMITS.trustMaxMs` (90 days). Trust is set
|
|
711
698
|
for `LIMITS.trustDefaultMs` (30 days) when the person names no end. An expired `trustedUntil` reads
|
|
712
699
|
as absent.
|
|
@@ -741,7 +728,7 @@ the person:
|
|
|
741
728
|
**Effective policy.** For a sender S, each level is, in order:
|
|
742
729
|
|
|
743
730
|
1. the person grant for S, where it sets the level;
|
|
744
|
-
2. otherwise, for `ask` and `agentMail`, the most permissive
|
|
731
|
+
2. otherwise, for `ask` and `agentMail`, the most permissive grant of a verified conversation shared with S;
|
|
745
732
|
3. otherwise the default for the relation:
|
|
746
733
|
|
|
747
734
|
| Relation | `chat` | `agentMail` | `ask` | trust |
|
|
@@ -766,7 +753,7 @@ from S.
|
|
|
766
753
|
- `sig` = Ed25519(`by`, `"messaging/1/card\n"` ‖ JCS(card without `sig`)). `by` is a device of `from`; `from` and
|
|
767
754
|
`to` are different persons. `trustedUntil` needs `ask: "open"`. A card is at most `LIMITS.cardBytes` of JCS.
|
|
768
755
|
- `can` is the person grant for S over the defaults of S's relation to R (core's `cardCan`): `chat` is true for
|
|
769
|
-
`open` and `muted`, `agentMail` for `open`.
|
|
756
|
+
`open` and `muted`, `agentMail` for `open`. Conversation grants are not on a card.
|
|
770
757
|
- `seq` rises with every card R's devices send S; S keeps the card with the highest `seq`.
|
|
771
758
|
- Core's `verifyContactCard` checks the schema, the rules, the size, the realm, both persons and the signature over
|
|
772
759
|
the raw value. Whether `by` is a current device of R is the caller's: a card signed by a device R no longer has
|
|
@@ -823,40 +810,27 @@ from S.
|
|
|
823
810
|
A delivered request then passes the inbound policy of §8.3 as any other;
|
|
824
811
|
- a request ends unanswered at its expiry: the sender's own expiry settles its side. A request
|
|
825
812
|
whose sender has left the realm ends at the answer, with nothing sent.
|
|
826
|
-
- **Message requests** (a directory realm only).
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
agents never send or answer one.
|
|
813
|
+
- **Message requests** (a directory realm only). Human first contact across orgs uses `contact.request` and a portable request proof (§16).
|
|
814
|
+
Its `message` envelope carries `conv: {kind: "request", proof}` and one text part, bounded by `LIMITS.requestChars`.
|
|
815
|
+
The envelope expiry must equal the signed descriptor expiry. Agents cannot send or answer contact requests.
|
|
830
816
|
- **Addressing.** The person gives an exact email. The daemon resolves it at the directory (`dir.resolve
|
|
831
817
|
{email}`, §7); no answer is `not_found`, "no Messaging user with that email accepts requests", the same for an
|
|
832
818
|
unknown and an undiscoverable email, so it cannot tell them apart. Several persons for one email is
|
|
833
819
|
`ambiguous_address`.
|
|
834
|
-
- **Sending.** The
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
from a blocked sender it is `refused`. A held request is stored with its envelope and opened payload (§8.8,
|
|
845
|
-
`contact_requests`), one per sending person and at most `LIMITS.heldTotal` in all (`rate_limited`), answered with a
|
|
846
|
-
`held` receipt and filed as a `request` notice (§8.2). It reaches no agent and wakes nothing. A request in a
|
|
847
|
-
roster realm is `invalid_request`: a roster realm is one org.
|
|
848
|
-
- **Answering** (`request.answer`): `accept` sets the sender's grant to `chat: "open"` (keeping its other
|
|
849
|
-
levels), which makes the sender a `contact` and sends them a card, then files the request's text as their
|
|
850
|
-
first line in `dm:<sender>` and answers `delivered`; `decline` answers `rejected {code: "declined"}`; `block`
|
|
851
|
-
sets `chat: "blocked"`, then declines. A request ends unanswered at its expiry. Every device of the person
|
|
852
|
-
holds its own copy: a grant for the sender that another device of the person signed, arriving by "Sync", settles
|
|
853
|
-
the copy held here as its answer did (`chat: "open"` accepts it, `blocked` declines it).
|
|
820
|
+
- **Sending.** The daemon sends one copy to each current recipient device and returns `{id,cid,person}`.
|
|
821
|
+
It stores the request separately from conversation history. An authenticated recipient receipt establishes an outgoing hold.
|
|
822
|
+
A missing receipt means pending delivery, not a hold. One request can wait per person, subject to `LIMITS.requestsPerDay`.
|
|
823
|
+
- **Receiving.** The daemon verifies the portable proof, envelope binding and expiry before it stores the request.
|
|
824
|
+
A stranger's request waits for the person. An allowed contact or same-org request follows the signed acceptance and import path automatically.
|
|
825
|
+
A blocked sender is refused. The opening reaches no agent and triggers no wake before or after import.
|
|
826
|
+
- **Answering.** `request.answer {answer:"accept"}` settles a signed mailbox decision, grants chat access, and imports exactly one opening into the deterministic DM.
|
|
827
|
+
The import uses the descriptor ID and permanent completion evidence. Concurrent accepting devices reconcile against the same verified operations and completion.
|
|
828
|
+
`decline` settles a signed decline. `block` also sets `chat:"blocked"`. A grant or card alone is not a terminal decision.
|
|
829
|
+
The daemon resumes interrupted acceptance after restart and sends a contact card after successful import.
|
|
854
830
|
- **The sender's card is the directory's.** Clients show the sender of a request by the realm's verified profile
|
|
855
831
|
(name, email, org, §15.1), never by the request's text.
|
|
856
|
-
- **Agents.**
|
|
857
|
-
|
|
858
|
-
`request.list` and filed as a `request` notice; the person sends the request with `contact.request`
|
|
859
|
-
(`suggestion` dismisses it), or dismisses it with `request.answer {answer: "decline"}`.
|
|
832
|
+
- **Agents.** Legacy contact suggestions remain dismissible. The removed `msg.send` method cannot create new suggestions.
|
|
833
|
+
Agent-to-agent DMs use recipient invitation admission instead of human contact requests.
|
|
860
834
|
- **Cards on the daemon.** A device sends a card (a `message` with `conv: {kind: "card", what: "card"}`) to each
|
|
861
835
|
current device of a person S when this person's grant for S changes on this device (`grant.set`, an answer to
|
|
862
836
|
a request, a re-sign under "Signers"), and when S asks with `card.get`. It sends none to a `stranger` and none to
|
|
@@ -867,14 +841,7 @@ from S.
|
|
|
867
841
|
person; it answers nothing. A `rejected` receipt from a person with `refused`, `request_required` or `declined`
|
|
868
842
|
drops their card and asks for a fresh one. The `seq` of a device's cards is its clock in milliseconds, raised
|
|
869
843
|
past the last one it used. A card is not a first message for the freshness bound (§15.9).
|
|
870
|
-
- **Pacing.**
|
|
871
|
-
limited to `LIMITS.a2hPerHour` (30) an hour and `LIMITS.a2hPerDay` (100) a day:
|
|
872
|
-
- the sending daemon refuses an agent's DM line or channel post at send time (`msg.send` to a person
|
|
873
|
-
or `#channel`), before it stores or sends anything, when it would take any recipient person past either
|
|
874
|
-
count, with `rate_limited` and when to try again; the person's own lines are not paced;
|
|
875
|
-
- the receiving daemon refuses an agent's line past either count from one sending person with a
|
|
876
|
-
`rejected` receipt, `rate_limited`, for a sender that does not pace itself;
|
|
877
|
-
- the reference daemon counts in memory, so a restart starts the windows again.
|
|
844
|
+
- **Pacing.** Conversation append limits replace legacy chat-envelope pacing (§16). Retained task envelopes still follow the relay and inbound limits (§7, §8.3).
|
|
878
845
|
- **Sync.** A device sends each grant it signs or re-signs to each of its person's other current
|
|
879
846
|
devices: a sealed `message` to the device with no agent, `conv: {kind: "self", what: "grant"}`,
|
|
880
847
|
whose one data part is `{grants}`. A device of the person that is new to the sender (one it has
|
|
@@ -1259,14 +1226,22 @@ Jelly ID, which wakes the host.
|
|
|
1259
1226
|
It replaces the device's earlier ticket, and is kept in the relay's store (`dir_wakes`, §8.8) until it is
|
|
1260
1227
|
used, disarmed, its device is unbound or erased, or it expires. At most 60 arms a device in a burst, then one a
|
|
1261
1228
|
minute (`rate_limited` with `retryAfterMs`). `wake.disarm {}` forgets the device's ticket.
|
|
1262
|
-
- **When the relay presents it.** Only
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
`
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1229
|
+
- **When the relay presents it.** Only while the device has no connection, and only for one of these:
|
|
1230
|
+
- **a conversation line by the device's own person to an armed agent.** The relay has just stored (`conv.ok`, §7
|
|
1231
|
+
"Conversation frames") a `msg` event whose `wake` hint (§16.2) names a session in `agents`. The person is the one
|
|
1232
|
+
of the authenticated device that appended the event (which `by.person` must match), never the event's claims
|
|
1233
|
+
alone, and only that person's current devices are considered. A person's own mail to their agent is such a line
|
|
1234
|
+
(design §8.2, "Wake"): their daemon sets `wake` to the sessions of their own agents a line addresses, a dm whose
|
|
1235
|
+
other end is one of them or a mention of one, and never names another person's session. A `wake` hint on another
|
|
1236
|
+
person's event reaches only that person's own devices, so it never wakes anyone else's host. A repeated append
|
|
1237
|
+
(`conv.ok` again for the same event) does not present again. The reason logged is `own`;
|
|
1238
|
+
- **an envelope** the relay has just taken (§7 "Routing": first seen, queued) for the device that is:
|
|
1239
|
+
- the person's own mail to an armed agent: header type `message`, `from.person` = `to.person`, and `to.agent` in
|
|
1240
|
+
`agents` (a task request, which keeps its envelope); or
|
|
1241
|
+
- an answer the device awaits: header type `task.status` or `task.artifact` whose `replyTo` is in `awaiting`.
|
|
1242
|
+
|
|
1243
|
+
Conversation events without a `wake` hint (posts, receipts, another person's lines), presence, task cancels, anyone
|
|
1244
|
+
else's mail, a message that merely names an awaited request, and anything for a connected device never wake. A request the recipient's person
|
|
1270
1245
|
must answer (§6.9) is held by the receiving daemon, which the relay cannot see: such mail never wakes unless it is
|
|
1271
1246
|
the person's own.
|
|
1272
1247
|
- **Presenting.** `POST <url>` with the form `client_assertion_type=urn:jelly:wake&ticket=<ticket>`, no redirect
|
|
@@ -1291,6 +1266,72 @@ Jelly ID, which wakes the host.
|
|
|
1291
1266
|
names them), and the ids of the task requests it awaits (envelope ids it already routed). A malicious relay can also
|
|
1292
1267
|
fail to wake a host, as it can drop any envelope; that delays the person's mail, it loses none.
|
|
1293
1268
|
|
|
1269
|
+
**Conversation frames** (`schema/conv-frames.ts`; §16). After `hello`, in a roster realm and in a directory realm
|
|
1270
|
+
alike, the relay sequences conversation logs. It is never trusted for authority: it sequences and filters, and
|
|
1271
|
+
members verify every entry (§16.4, §16.7). Every frame is strict; a refusal is `nack {id: req, code, message}`.
|
|
1272
|
+
|
|
1273
|
+
- **`conv.append {req, event}`**, an `op` or a `msg` (a `keys` event is `invalid_request`: shares go in
|
|
1274
|
+
`conv.share`). The relay checks, in order:
|
|
1275
|
+
1. the frame's schema and `LIMITS.convEventBytes` (`too_large`); `by.device` is the authenticated device
|
|
1276
|
+
(`forbidden`);
|
|
1277
|
+
2. the rate limits below (`rate_limited` with `retryAfterMs`);
|
|
1278
|
+
3. a `create` against an empty log first, so a person who could not create it learns nothing of an existing one;
|
|
1279
|
+
then a `create` of an existing conversation is `conv_conflict`;
|
|
1280
|
+
4. a person who is not a participant person of an existing conversation gets `not_found`, as for a conversation
|
|
1281
|
+
that does not exist (no existence oracle for derived dm ids), except for a `join`;
|
|
1282
|
+
5. `ConvLog.apply` against the relay's copy of the log at its head, with the live checks (§16.7, step 4): `ts`
|
|
1283
|
+
within the clock skew, `by.device` a current device of `by.person`, and the org of a person for `join` and
|
|
1284
|
+
`add`. In a directory realm the devices and orgs are the directory's (§15.10); in a roster realm they are the
|
|
1285
|
+
roster's, and the realm is one org, its team id. An `op` whose `opHead` is not the current op head is
|
|
1286
|
+
`conv_conflict`; a `msg` under another epoch, or while a rekey is due, is `stale_epoch`;
|
|
1287
|
+
6. every person a `create` or `add` names has a current device in the realm (`not_found`), and no person would
|
|
1288
|
+
take part in more than `LIMITS.convsPerDevice` (5000) conversations (`too_large`).
|
|
1289
|
+
|
|
1290
|
+
It then assigns `seq`, stores the entry and answers `conv.ok {req, cid, seq, hash}` once it is committed (the
|
|
1291
|
+
group commit, as for `send`), and pushes `conv.push {cid, entry}` to the other online devices of every participant
|
|
1292
|
+
person before and after the entry, so a removed person's devices see their removal. A repeat of an event the log
|
|
1293
|
+
holds (the same JCS) is answered `conv.ok` again, with its first `seq`; another event with a used `id` is
|
|
1294
|
+
`duplicate`.
|
|
1295
|
+
- **`conv.share {req, events}`**: 1 to `LIMITS.convSharesPerFrame` (100) key shares (§16.6), all of one
|
|
1296
|
+
conversation and by the authenticated device. Each must pass `ConvLog.checkShare` live, with `opHead` the current
|
|
1297
|
+
op head (`conv_conflict`), so a person removed since gets none. All or none are stored; the relay answers
|
|
1298
|
+
`conv.shared {req, cid, count}` and pushes `conv.push {cid, share: {n, event}}` to each recipient device online,
|
|
1299
|
+
and to no other device. A previously stored share repeated with identical JCS is acknowledged without storing or
|
|
1300
|
+
pushing it again, even if the op head has since advanced. A used id with different content is `duplicate`. A new
|
|
1301
|
+
share must still pass all current checks; the repeat rule never delivers an old share to a removed device.
|
|
1302
|
+
- **`conv.fetch {req, cid, after, limit?, sharesAfter?}`** → `conv.entries {req, cid, head, entries, shares, more}`:
|
|
1303
|
+
the entries after `after` in `seq` order, at most `limit` (`LIMITS.convFetchEntries` (500)) and as many as fit in
|
|
1304
|
+
three quarters of a frame (at least one). An expired `msg` is served as `{seq, prev, digest}`; an op never is. With
|
|
1305
|
+
`sharesAfter`, also this device's shares after that `n`; no other device's share is ever served. `more` says
|
|
1306
|
+
there is more of either. Only a device of a participant person may fetch (`not_found` otherwise).
|
|
1307
|
+
- **`conv.head {req, cids? | after?}`** → `conv.heads {req, heads: [{cid, head, opHead, shares}], more}`: the heads
|
|
1308
|
+
of the named conversations this device's person takes part in, or, without `cids`, of all of them by `cid` after
|
|
1309
|
+
`after`, at most `LIMITS.convHeads` (500) each way. `shares` is the last share `n` for this device (0: none). A
|
|
1310
|
+
device that comes back online asks this first, then fetches what moved.
|
|
1311
|
+
- **`conv.caps {req, persons}`** → `conv.capabilities {req, outdated}`: of at most `LIMITS.convOpParticipants` named
|
|
1312
|
+
persons, those every one of whose current devices with a stored, unexpired presence names no `conversations`
|
|
1313
|
+
feature: a daemon from before conversations, which reads no log. A person with no such presence is not named, since
|
|
1314
|
+
no presence is no evidence. It is a hint from the relay and can only make a sender refuse: before it creates a dm,
|
|
1315
|
+
group or channel with that person, adds them, or sends them a message request, a daemon asks, and refuses with
|
|
1316
|
+
`peer_outdated` ("<handle> needs to update Messaging"), creating and sending nothing. It reveals one bit about a
|
|
1317
|
+
person of the realm, whom the asker can already name.
|
|
1318
|
+
- **Retention.** Once a minute the relay prunes the bodies of `msg` entries older than the conversation's
|
|
1319
|
+
`retainDays` (as of its head), by the time it sequenced them, keeping each entry's `seq`, `prev` and digest
|
|
1320
|
+
(§16.4), and deletes the key shares older than it. It never prunes an op.
|
|
1321
|
+
- **Limits** (per conversation and device, in memory; a restart refills them):
|
|
1322
|
+
- appends by one device to one conversation: a burst of `LIMITS.convAppendBurst` (20), refilled at
|
|
1323
|
+
`LIMITS.convAppendsPerMinute` (60) a minute; by all its devices, `LIMITS.convAppendsPerConvPerMinute` (600) a
|
|
1324
|
+
minute;
|
|
1325
|
+
- `rekey` ops whose cause the log cannot prove (`device`, `schedule`): `LIMITS.convRekeysPerHour` (4) per device
|
|
1326
|
+
per conversation an hour;
|
|
1327
|
+
- creates: `LIMITS.convCreatesPerHour` (60) per device an hour;
|
|
1328
|
+
- key shares: `LIMITS.convSharesPerMinute` (10,000) per device per conversation a minute, and at most
|
|
1329
|
+
`LIMITS.convSharesPerPair` (256) kept from one sender device for one recipient device in one conversation
|
|
1330
|
+
(`rate_limited`), so one member cannot fill another's shares.
|
|
1331
|
+
- **What the relay learns.** The plaintext ops (participants, roles, settings, epochs, policies), the kind of each
|
|
1332
|
+
sealed op (`meta`, or a `setting`'s kind), sizes and timing. Never a message body, a name, a topic, a setting's
|
|
1333
|
+
content or a key.
|
|
1334
|
+
|
|
1294
1335
|
**What the relay can see.** Team ids, machine ids, persons, session names, timing, and presence:
|
|
1295
1336
|
its session names, agent kinds, states and tiers, and the repo, branch and doing text a machine
|
|
1296
1337
|
chooses to share (§6.5, none by default). It never sees message, task or receipt bodies.
|
|
@@ -1354,6 +1395,30 @@ chooses to share (§6.5, none by default). It never sees message, task or receip
|
|
|
1354
1395
|
|
|
1355
1396
|
### 8.2 IPC API
|
|
1356
1397
|
|
|
1398
|
+
**Conversation render schemas.** Core exports `ParticipantView`, `NoticeView`, `FileView`, `TaskView`, `EventView`, `ConvView`, `ActivityBody` and `ConvStreamEvent` (§16).
|
|
1399
|
+
Names and content are untrusted peer data. A `FileView` exposes no attachment key or nonce.
|
|
1400
|
+
A deleted `EventView` contains neither text nor files. Lane 4 omits read positions, preferences and unread counts.
|
|
1401
|
+
Stream records carry a daemon-local `pos`. An `event.update` names the conversation and event of its embedded projection.
|
|
1402
|
+
Stream record types are `event`, `event.update`, `conv`, `member`, `read`, `typing`, `activity` and `gap`.
|
|
1403
|
+
An `event.update` change is one of `edit`, `delete`, `react`, `pin`, `replies`, `delivery` and `task`; lane 4 emits `edit`, `delete`, `react`, `pin` and `delivery`.
|
|
1404
|
+
A `request.waiting` item's state is one of `held` and `pending-delivery`.
|
|
1405
|
+
The daemon re-reads verified state before it sends a projection.
|
|
1406
|
+
|
|
1407
|
+
**Staged conversation methods.** These contracts have no live handler: `conv.catalogue`, `conv.publish`, `conv.markRead`, `conv.markUnread`, `conv.prefs`, `conv.follow`, `conv.save`, `conv.saved`, `conv.draft`, `conv.schedule`, `conv.scheduled`, `conv.unschedule`, `conv.typing`, `context.get`, `context.set`, `context.set.preview`, `conv.localRetention`, `file.get`, `session.activity`, `turn.cancel`.
|
|
1408
|
+
|
|
1409
|
+
**Live conversation methods.** `LIVE_CONVERSATION_METHODS` is part of `IPC_METHODS`. The tier table applies on both sockets.
|
|
1410
|
+
Batch `people.get` accepts up to 100 distinct person IDs and returns `{people, missing}` without device lists.
|
|
1411
|
+
`people.get` and `sessions.list` work without a connected conversation engine.
|
|
1412
|
+
Human-only `request.waiting` reports authenticated outgoing holds separately from pending delivery. `request.list` reports incoming requests.
|
|
1413
|
+
A `conv.send` names exactly one of `cid` and `to`. `broadcast` requires a thread.
|
|
1414
|
+
A page chooses at most one position. Agent calls require a session registered on their connection.
|
|
1415
|
+
The daemon enforces access, audience consent and own-line restrictions after asynchronous work too.
|
|
1416
|
+
Creation requires two DM participants or 3–50 group participants. Only channels have visibility. Only agents can be seated.
|
|
1417
|
+
`conv.access` pages accessible conversations. `conv.sync` waits at most 25 seconds and returns at most 50 events.
|
|
1418
|
+
A restart or expired replay position produces a gap. Clients then reload their accessible conversations.
|
|
1419
|
+
`conv.backfill` returns `complete: false` until indexing coverage accounts for missing keys and expired entries.
|
|
1420
|
+
The method schemas and their error arrays are normative. Malformed JSON-RPC and params use JSON-RPC errors.
|
|
1421
|
+
|
|
1357
1422
|
The API is the method table `IPC_METHODS` in `schema/ipc.ts`: JSON-RPC 2.0 NDJSON on two Unix
|
|
1358
1423
|
sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (for example
|
|
1359
1424
|
`contacts.list`, `item.read` and `task.get`), the TSDoc in that file and in `schema/ipc/*.ts` is normative.
|
|
@@ -1366,8 +1431,8 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1366
1431
|
- **Two sockets.** The daemon serves the one API on two sockets, and itself enforces what each
|
|
1367
1432
|
serves (its tiers). A client's own method list is a second check, not the boundary.
|
|
1368
1433
|
- `run/agent.sock` (the agent tier) serves only `AGENT_METHODS`. Any other method fails with
|
|
1369
|
-
`forbidden` before its params are read.
|
|
1370
|
-
|
|
1434
|
+
`forbidden` before its params are read. Conversation calls on this socket require a bound session.
|
|
1435
|
+
They cannot send as the person. Agent-facing clients connect here: the OpenCode plugin's server part
|
|
1371
1436
|
and `messaging-mcp`.
|
|
1372
1437
|
- `run/admin.sock` (the admin tier) serves every method. Human clients connect here: the `messaging`
|
|
1373
1438
|
CLI and the `/messaging` TUI.
|
|
@@ -1421,7 +1486,7 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1421
1486
|
- **What the tiers are, and what they are not.** Where the daemon, OpenCode and the user's shell
|
|
1422
1487
|
run as one OS user, as on a laptop, the tiers are not a security boundary: any process of that
|
|
1423
1488
|
user can open `run/admin.sock`. They keep `team.*`, `identity.rotateEnc`, `daemon.stop`,
|
|
1424
|
-
|
|
1489
|
+
conversation administration and the person's own chat lines off every tool path. A tool that
|
|
1425
1490
|
speaks to the agent socket cannot reach them. A shell that runs the `messaging` CLI is still a shell,
|
|
1426
1491
|
and only the host's shell permissions govern it. The tiers are a boundary only when `messagingd`
|
|
1427
1492
|
runs as its own user and the engine runs as another user in `ipc.agentGroup` (the layout is in
|
|
@@ -1430,23 +1495,25 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1430
1495
|
as them and call every method, including `team.*` and `daemon.stop`. Agent-facing clients still
|
|
1431
1496
|
must not expose `team.*`. The OpenCode TUI is a human surface and may call them. It must not
|
|
1432
1497
|
place an invite ticket in a session, a log, or a model context.
|
|
1433
|
-
- Key-scoped methods (`session.update`, `session.unregister`, `events.subscribe`, `
|
|
1434
|
-
`
|
|
1435
|
-
|
|
1498
|
+
- Key-scoped methods (`session.update`, `session.unregister`, `events.subscribe`, `inbox.*`, `item.read`, `task.*`,
|
|
1499
|
+
`conv.list`, `conv.read`, `conv.send`, `conv.react`, `conv.edit`, `conv.delete`, `conv.search`, `conv.context`, `conv.access`, `conv.sync`)
|
|
1500
|
+
work only for a key that the calling connection registered. Human conversation calls can omit the key on the admin socket.
|
|
1436
1501
|
- A session key is therefore a bearer credential for its session: whoever knows it can register it
|
|
1437
1502
|
and act as that session.
|
|
1438
1503
|
- `AGENT_METHODS` (with its type `AgentMethod`) lists the methods an agent-facing client may call,
|
|
1439
1504
|
and exactly the methods `run/agent.sock` serves. Anything else is for human clients: the
|
|
1440
1505
|
`messaging` CLI and the `/messaging` TUI.
|
|
1441
|
-
- **Tiers.**
|
|
1506
|
+
- **Tiers.** Conversation methods available to agents also appear in the human list:
|
|
1442
1507
|
`AGENT_METHODS` (the agent tier: sessions, `device.status`, `device.bind`, `contacts.list`,
|
|
1443
|
-
`
|
|
1508
|
+
`inbox.list`, `inbox.get`, `inbox.ack`, `item.read`, `conv.list`, `conv.read`, `conv.send`, `conv.react`, `conv.edit`, `conv.delete`, `conv.search`, `conv.context`, `conv.access`, `conv.sync`, `task.*`,
|
|
1444
1509
|
`events.subscribe`, `daemon.status`, `daemon.health`); `HUMAN_METHODS` (acting as the person: `conv.list`,
|
|
1445
|
-
`conv.read`, `conv.
|
|
1510
|
+
`conv.read`, `conv.send`, `conv.react`, `conv.edit`, `conv.delete`, `conv.search`, `conv.context`, `conv.access`, `conv.sync`,
|
|
1511
|
+
`conv.get`, `conv.members`, `conv.threads`, `conv.pin`, `conv.create`, `conv.add`, `conv.remove`, `conv.role`, `conv.seat`,
|
|
1512
|
+
`conv.settings`, `conv.meta`, `conv.archive`, `conv.leave`, `conv.join`, `conv.accept`, `conv.policy`, `conv.backfill`, `people.search`, `people.get`,
|
|
1446
1513
|
`grant.list`, `grant.set`, `grant.set.preview`, `request.list`, `request.answer`, `request.answer.preview`, `contact.request`,
|
|
1447
|
-
`contact.request.preview`, `card.get`, `
|
|
1448
|
-
`
|
|
1449
|
-
`device.remove.preview`, `identity.replace.preview`, `directory.erase.preview
|
|
1514
|
+
`contact.request.preview`, `card.get`, `sessions.list`, `request.waiting`,
|
|
1515
|
+
`notice.subscribe`, `notice.list`, `notice.ack`, `device.approve.preview`,
|
|
1516
|
+
`device.remove.preview`, `identity.replace.preview`, `directory.erase.preview`); and `ADMIN_METHODS`
|
|
1450
1517
|
(this device and the daemon: `daemon.stop`,
|
|
1451
1518
|
`daemon.woke`, `person.attached`, `daemon.snapshot`, `wake.arm`, `wake.disarm`,
|
|
1452
1519
|
`device.list`, `device.remove`, `device.link`, `device.login`, `device.login.status`, `device.login.cancel`,
|
|
@@ -1561,12 +1628,8 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1561
1628
|
`PeerRef` (inbox items, task records), provenance (§6.6), directory entries (whose client kind is
|
|
1562
1629
|
`client`) and chat lines. `daemon.status` names this device in `identity` (its `device`), and a divergence alarm
|
|
1563
1630
|
the signer's `device`. Team administration (`team.*`) keeps the roster's `machine`.
|
|
1564
|
-
- **Persons in the API.**
|
|
1565
|
-
|
|
1566
|
-
`contacts.list`, `people.*`, a `dm:` or `agent:` conversation, and a line's `to` in `msg.send`), it
|
|
1567
|
-
takes a `PersonId`: a roster person name or a hosted `p_…` id. Channel membership (`channel.*`, and the
|
|
1568
|
-
`persons` and `creator` of a channel op, §6.8) takes `PersonId`s too. Team administration (`team.*`) takes
|
|
1569
|
-
roster person names.
|
|
1631
|
+
- **Persons in the API.** Person references use `PersonId`: a roster person name or a hosted `p_…` ID.
|
|
1632
|
+
Conversation participants use person IDs or stable `person/agent` addresses. Team administration uses roster person names.
|
|
1570
1633
|
- **`session.register`.** Names a session.
|
|
1571
1634
|
- The requested name is suffixed `-2`, `-3`, ... if it is taken on this machine.
|
|
1572
1635
|
- Registering an existing key again updates it; this is idempotent.
|
|
@@ -1607,24 +1670,14 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1607
1670
|
expired `trustedUntil` is left out. So an agent sees whom it can reach without a human-tier call. A `reach` also
|
|
1608
1671
|
carries `devicesChangedAt` while a device of that person was added, removed or replaced less than 7 days ago, as
|
|
1609
1672
|
their verified log (or the roster) shows it; a rekey or an attest is not a change, and it carries no other text. Entries
|
|
1610
|
-
carry `expect`: the hint a `
|
|
1673
|
+
carry `expect`: the hint a `task.create` to that session, with the wake hint set, returns
|
|
1611
1674
|
(below), computed from the same view. Agent clients show it before anything is sent. It is absent
|
|
1612
1675
|
when no presence lists the session.
|
|
1613
|
-
- **`
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
at most `LIMITS.chatChars`, and `replyTo` as the thread (the `id` of any line of it); any other
|
|
1617
|
-
A2A field (`contextId`, `taskId`, `messageId`, `metadata`, `extensions`, `referenceTaskIds`,
|
|
1618
|
-
`onBehalfOf`, `wake`) is `invalid_request`, and so is a `to` that is neither a person
|
|
1619
|
-
name nor `#` and a channel name. It fails with `not_found` for a person not on the team, a
|
|
1620
|
-
channel this person is not in, or an unknown thread, and `too_large` for a longer line. It writes
|
|
1621
|
-
the line as `person.send` does (below), and `idempotencyKey` is the line's `clientId`. Its one
|
|
1622
|
-
result is `{to, envelopeId: <line id>, messageId, contextId, stage: "accepted"}`. With `request:
|
|
1623
|
-
{contact: <email>}`, only on a line to the session's own person (otherwise `invalid_request`), the daemon
|
|
1624
|
-
also files a suggestion for the person (§6.9, "Message requests").
|
|
1625
|
-
- **To `person/session[@label]`: mail or a task** to that session, as the rest of this item says.
|
|
1676
|
+
- **`task.create` and `task.continue`.** These methods retain task envelopes until lane 7.
|
|
1677
|
+
The destination is `person/session[@label]`. Creation assigns a task ID and signs a delegation chain.
|
|
1678
|
+
Continuation requires the existing task ID and its bound executor. Both require the calling session's `key`.
|
|
1626
1679
|
- Refuses content that holds a credential (`secret_refused`, §8.7), as do `task.update`,
|
|
1627
|
-
`task.cancel` and `
|
|
1680
|
+
`task.cancel`, `conv.send` and `conv.edit`.
|
|
1628
1681
|
- Resolves the address against the directory, including offline sessions.
|
|
1629
1682
|
- Builds one envelope per recipient and stores it in the outbox (`accepted`).
|
|
1630
1683
|
- Sends it when the relay is connected.
|
|
@@ -1650,14 +1703,12 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1650
1703
|
`idempotencyKey` returns the hint computed for the original call.
|
|
1651
1704
|
- `idempotencyKey` returns the original results for a repeated call from the same key.
|
|
1652
1705
|
- `wake` defaults to true.
|
|
1653
|
-
-
|
|
1654
|
-
- A `kind: "task"` send signs a root grant for each recipient (§6.7).
|
|
1706
|
+
- Creation assigns ULID task IDs and a `contextId` when omitted, and signs a root grant for each recipient (§6.7).
|
|
1655
1707
|
- A chain this daemon already knows the recipient would refuse is not sent. The result is
|
|
1656
1708
|
`stage: "rejected"` with that check's code, and the requester's task is stored
|
|
1657
1709
|
`TASK_STATE_REJECTED` with no grants.
|
|
1658
1710
|
- **`onBehalfOf {taskId, scope?, maxDepth?}`** delegates on behalf of a task that the calling key
|
|
1659
|
-
**executes**, and only with
|
|
1660
|
-
or `invalid_request` (`kind` is not `"task"`), before anything is stored. The daemon:
|
|
1711
|
+
**executes**, and only with creation. Any other task fails with `not_found` before storage. The daemon:
|
|
1661
1712
|
1. refuses a terminal task (`invalid_state`) before anything is stored;
|
|
1662
1713
|
2. signs the next grant of the stored chain for each recipient, narrowed to `scope` and
|
|
1663
1714
|
`maxDepth` when given;
|
|
@@ -1680,101 +1731,40 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1680
1731
|
- **`task.cancel`** is allowed only for the requester.
|
|
1681
1732
|
- **`task` events** carry `origin`:
|
|
1682
1733
|
- `local` when the event follows the subscriber key's own call: `task.update`, `task.cancel`,
|
|
1683
|
-
or the `
|
|
1734
|
+
or the `task.create` call that created the record;
|
|
1684
1735
|
- `peer` when it follows an envelope from the other side, a receipt, or a daemon timeout.
|
|
1685
1736
|
- When the event follows a status or artifact update, `update` carries that delta, including
|
|
1686
1737
|
`append` and `lastChunk`.
|
|
1687
|
-
- **`conv.read {key?,
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
"agent"`), which needs the session `key`: what the session `key` exchanged with one other session,
|
|
1691
|
-
on any machine of that person: incoming mail,
|
|
1692
|
-
outgoing mail and tasks either way, as `ConvEntry` rows in time order. A task request appears
|
|
1693
|
-
once, as its task (dated when it was created, with its current state), not again as mail. A
|
|
1694
|
-
cursor is an entry's `{at, id}` with an agent, and a line's `seq` in a DM or a channel; the other
|
|
1695
|
-
form is `invalid_request`, and so is `thread` with an agent. With no cursor it is the latest `limit` entries (default 30, at
|
|
1696
|
-
most 100); with `before` the latest `limit` before that place; with `after` the earliest `limit`
|
|
1697
|
-
after it. Entries come oldest first, and `more` says entries remain past the page in the
|
|
1698
|
-
direction read; `before` and `after` together are invalid params. Incoming mail carries its
|
|
1699
|
-
preview and stage, a task the start of its request, the session's role and the task's state.
|
|
1700
|
-
Outgoing mail carries its stage but no text: the daemon keeps no copy of what it sent. It changes
|
|
1701
|
-
no stage and sends no receipt. Agent clients build their conversation view on it (§11).
|
|
1738
|
+
- **`conv.read {key?, cid, thread?, before?, after?, around?, limit?, cursor?}`** returns current event projections, oldest first, or an explicit context hold.
|
|
1739
|
+
Pages contain at most 50 events. Before and after positions are log sequence numbers. An opaque cursor binds to the query and session.
|
|
1740
|
+
Reads expose no legacy mail or fabricated task events. Retained tasks use their own methods.
|
|
1702
1741
|
- **`task.list`:** most recently updated first. It filters by `role`, `state`, `contextId`,
|
|
1703
1742
|
`peer` (a qualified address) and `updatedAfter` (at or after, ≥), pages with an opaque `cursor`/`nextCursor`, and reports `total`.
|
|
1704
|
-
-
|
|
1743
|
+
- **Task A2A passthrough:** `messageId` (generated if omitted), `metadata` (at most
|
|
1705
1744
|
16,384 bytes of UTF-8 JSON, which for wire JSON is its JCS length; the daemon always overwrites `metadata[urn:messaging:ext:provenance:v1]` on the receiving
|
|
1706
1745
|
side), `extensions`, and `referenceTaskIds`.
|
|
1707
|
-
- **
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
- `conv.markRead {conversation, upTo}` moves the conversation's read cursor to `upTo`, a line's `seq`, if that is
|
|
1719
|
-
later, clamped to the newest `seq` here. It returns `{readUpTo, unread}`. It is a human-client
|
|
1720
|
-
method that is not in `AGENT_METHODS`: read state is the person's, and an agent reading a
|
|
1721
|
-
conversation does not mark it read. It fails with `not_found` for a channel this person is not in.
|
|
1722
|
-
- A `ChatLine` this machine sent to other persons carries `state` (`sent`, `delivered` or
|
|
1723
|
-
`refused`) and, when refused, `refused` (the persons). A line the daemon wrote itself, about a
|
|
1724
|
-
channel change or a refusal of one, carries `notice: true`.
|
|
1725
|
-
- `person.send {conversation, text, thread?, clientId?}` (human tier) writes the person's own line
|
|
1726
|
-
and its envelopes in one transaction and sends them at once, with no session. It fails with
|
|
1727
|
-
`not_found` for a person not on the team or a channel this person is not in. A DM to oneself
|
|
1728
|
-
is the person's own conversation (§6.8).
|
|
1729
|
-
- `search {query, conversation?, limit?}` finds lines in the DMs and channels this person holds,
|
|
1730
|
-
newest first, as `hits`, each with its `conversation`.
|
|
1731
|
-
- **Pacing.** The sender keeps its chat envelopes to one recipient machine below that machine's
|
|
1732
|
-
chat budget (§8.3): the reference daemon allows 50 a minute, ten below
|
|
1733
|
-
`LIMITS.inboundPerMachinePerMinute`, for clock and transit jitter. Channel changes count too. A
|
|
1734
|
-
line (`person.send`, or `msg.send` to a person or a channel) that would go over it for any machine
|
|
1735
|
-
it goes to fails with `rate_limited`, a
|
|
1736
|
-
message that says when to try again, and nothing stored or sent. Pacing cannot cover a backlog:
|
|
1737
|
-
lines queued at the relay for an offline machine reach it together when it connects. The
|
|
1738
|
-
receiver defers those past its budget and the relay paces them (§7 "Delivery", §8.3), so every
|
|
1739
|
-
line is delivered within minutes; only one that would expire first is refused.
|
|
1740
|
-
- A line with a `clientId` (`msg.send`'s `idempotencyKey`) is idempotent per `clientId` and
|
|
1741
|
-
sender: the session `key`, or the person for `person.send`. A repeat with the same room, thread and text returns the first call's
|
|
1742
|
-
result and files and sends nothing. The same `clientId` for another line is `invalid_request`.
|
|
1743
|
-
The daemon keeps a `clientId` for `LIMITS.ttlMs`.
|
|
1744
|
-
- `channel.create`, `channel.invite`, `channel.leave` and `channel.remove` are for the human
|
|
1745
|
-
`/messaging` TUI. They take a channel name as this machine holds it (its local name). Each checks
|
|
1746
|
-
before it stores anything. `channel.create` refuses a name already held, more than the
|
|
1747
|
-
channels a machine holds (`invalid_request`), an unknown person, or more than
|
|
1748
|
-
`LIMITS.channelPersons` persons with the creator. `channel.remove` refuses a caller who is not
|
|
1749
|
-
a member (`not_found`), who is neither the creator nor a team admin (`forbidden`), a list that
|
|
1750
|
-
names the caller (`invalid_request`: use `channel.leave`), and a person who is not a member
|
|
1751
|
-
(`not_found`).
|
|
1752
|
-
- Mail and tasks (`msg.send` to an agent) never write the transcript.
|
|
1753
|
-
- **`conv` events** go only to connections that called `conv.subscribe`, a human-client method
|
|
1754
|
-
that is not in `AGENT_METHODS`. They are never sent to `events.subscribe` keys. A `conv` event
|
|
1755
|
-
names the conversation, the line's `id` and its person, never its text. It is raised for a filed line,
|
|
1756
|
-
a channel change, and a change of an outgoing line's delivery state (then `person` is this
|
|
1757
|
-
machine's person). Each carries the `seq` of its change (§6.8), so events rise in `seq`.
|
|
1758
|
-
- **Resuming.** `conv.subscribe {since}`, where `since` is the `seq` of the last event the client
|
|
1759
|
-
handled, first sends one `conv` event per line that changed after `since`, oldest change first,
|
|
1760
|
-
each with the `seq` of the line's latest change. Then live events follow, with none between.
|
|
1761
|
-
It returns `{seq, gap}`: the newest `seq` at the call, and whether the replay could not be
|
|
1762
|
-
complete, in which case it sends none. That is when more than a policy number of lines changed
|
|
1763
|
-
(the reference daemon: 500), a channel was deleted here after `since`, or `since` is above
|
|
1764
|
-
`seq` (a store set up again). After a gap the client reloads with `conv.list` and `conv.read`.
|
|
1765
|
-
Without `since`, `gap` is false and no events are replayed.
|
|
1746
|
+
- **Conversations (§6.8).** The agent-accessible methods are `conv.list`, `conv.read`, `conv.send`, `conv.react`, `conv.edit`, `conv.delete`, `conv.search`, `conv.context`, `conv.access` and `conv.sync`.
|
|
1747
|
+
Every agent call uses its bound session. Human calls use the admin socket.
|
|
1748
|
+
A `conv.send` returns `{cid, id, clientId, stage: "pending"}` or an explicit audience hold.
|
|
1749
|
+
The durable outbox retries identical signed events. A repeated `clientId` returns the first event ID.
|
|
1750
|
+
A queued key share whose recipient device the conversation's own verified log removes (an accepted `remove` of its
|
|
1751
|
+
person or the person's own `leave`, with the person no longer a participant, or a `rekey {cause: device}` naming
|
|
1752
|
+
the device) is dropped, never sent, and logged by conversation, device and event id. Any other share to a device
|
|
1753
|
+
that is no longer current, such as a directory miss, holds the outbox until the state is verified.
|
|
1754
|
+
Reuse with different content or a different destination fails with `invalid_request`.
|
|
1755
|
+
Conversation administration uses the human-tier methods in the tier list. Internal notifications carry identifiers only.
|
|
1756
|
+
The stream re-renders current verified state, including edits and deletions, for each reader.
|
|
1766
1757
|
- **Consent (§6.9).** `grant.list`, `grant.set`, `grant.set.preview`, `request.list`,
|
|
1767
1758
|
`request.answer`, `request.answer.preview`, `contact.request`, `contact.request.preview` and `card.get` are human-client methods that are
|
|
1768
1759
|
not in `AGENT_METHODS`: agents never read or change a person's grants, answer a held request, or send a message
|
|
1769
1760
|
request.
|
|
1770
1761
|
- `grant.list {subject?}` returns this person's grants as this device holds them, newest first,
|
|
1771
1762
|
each with its levels, `id`, `ts`, `by` and, when re-signed, `resignedAt`, and the review list:
|
|
1772
|
-
the widening levels that lapsed with their signer, with when they lapsed. A
|
|
1773
|
-
carries the channel id and, while this person is in it, its local name.
|
|
1763
|
+
the widening levels that lapsed with their signer, with when they lapsed. A conversation subject carries its canonical `cid`.
|
|
1774
1764
|
- `grant.set {subject, chat?, agentMail?, ask?, trustedUntil?, trust?, discoverable?}` signs the
|
|
1775
1765
|
whole new grant for the subject with this device: a level left out is the relation's default,
|
|
1776
|
-
and no level at all is a clear. `subject` is `{person}` or `{
|
|
1777
|
-
|
|
1766
|
+
and no level at all is a clear. `subject` is `{person}` or `{conv: cid}`.
|
|
1767
|
+
`trust: true` without `trustedUntil` trusts for `LIMITS.trustDefaultMs`. It stores the
|
|
1778
1768
|
grant, takes the subject off the review list and sends the grant to this person's other devices
|
|
1779
1769
|
in one transaction. It fails with `not_found` for a person the realm does not know or a channel
|
|
1780
1770
|
this person is not in, `invalid_request` for a grant that breaks a rule of §6.9, and
|
|
@@ -1842,27 +1832,12 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1842
1832
|
`daemon.snapshot {done: true}` deletes it and returns `{released}` (false when none was held). One is held at a time
|
|
1843
1833
|
(`invalid_state` while one is); the daemon releases it after an hour (`until`), when it stops, and one a previous run
|
|
1844
1834
|
left when it starts.
|
|
1845
|
-
- **Your agents.** `
|
|
1846
|
-
|
|
1847
|
-
- `agents.list` returns each local session (registered or kept) as `{agent, conversations, unread,
|
|
1848
|
-
lastAt?, state}`: how many other agents it has mail or tasks with, the mail it has not read, its last
|
|
1849
|
-
traffic, and `idle`, `busy` or `kept`; most recent first.
|
|
1850
|
-
- `agents.read {agent, conversation?, before?, limit?}` returns, without `conversation`, `{kind:
|
|
1851
|
-
"conversations", conversations}`: each `agent:<person>/<agent>` it has traffic with, with its unread
|
|
1852
|
-
mail and last time, most recent first. With `conversation`, it returns `{kind: "entries",
|
|
1853
|
-
conversation, entries, more}`: a page of entries as `conv.read` gives them to that agent (default
|
|
1854
|
-
30, latest first, `before` an entry's `{at, id}`). It changes no stage and sends no receipt.
|
|
1855
|
-
`not_found` for an agent this device has no session of. Peer text is untrusted; a client cleans it
|
|
1856
|
-
for display (`cleanPeerText`) and never shows it to a model.
|
|
1835
|
+
- **Your agents.** Human-tier `sessions.list` returns local sessions as `{agent, state, seats}`.
|
|
1836
|
+
State is `idle`, `busy` or `kept`. Seats are verified conversation IDs. No unread count is fabricated.
|
|
1857
1837
|
- **Notices.** What the person is told about, in three fixed categories, as `{id, category,
|
|
1858
1838
|
conversation?, from?: {name, email?, org}, line?, at}`:
|
|
1859
|
-
- `message`: a
|
|
1860
|
-
|
|
1861
|
-
about a channel change, or a change of a line's delivery state. Its `conversation` is the DM or the
|
|
1862
|
-
channel, and `from` the sender's person as the realm verified them.
|
|
1863
|
-
- `request`: a request held for the person (§6.9), with `conversation` the sender's
|
|
1864
|
-
`agent:<person>/<agent>` (`dm:<person>` when the person sent it themself, as for a message request); or a
|
|
1865
|
-
suggestion of one of the person's own agents, with `conversation` `agent:<own person>/<agent>`.
|
|
1839
|
+
- `message`: a retained legacy notice, without transcript text. Live conversation updates use the conversation stream.
|
|
1840
|
+
- `request`: a held request or retained contact suggestion (§6.9). New notices contain no legacy room reference.
|
|
1866
1841
|
- `alert`: an alarm of this device (`keys_missing`, `restored`, `directory`), with fixed text in `line`; a
|
|
1867
1842
|
change of another device of the person (below), with `device`; or trust the person gave someone that ends
|
|
1868
1843
|
within 7 days (§6.9), with `from` that person and `line` "Trust you gave this person ends on <date>; renew it
|
|
@@ -1887,13 +1862,11 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1887
1862
|
was replaced by 'desk2' (jelly-host), ...", "'phone' (laptop) was removed from your account.".
|
|
1888
1863
|
- Each notice is filed once, when its source is, under an id derived from the source (`m_<line id>`,
|
|
1889
1864
|
`r_<request id>`, `a_<alarm and time>`, `a_dev<log seq>`, `a_trust<grant id>`), so a replay repeats the same ids. A notice copies no text:
|
|
1890
|
-
|
|
1891
|
-
through `cleanPeerText`, at most 120 characters with its "…".
|
|
1865
|
+
`lines: true` can expose fixed alert text. It never restores legacy transcript content.
|
|
1892
1866
|
- `notice.subscribe {after?, lines?}` sends the unacknowledged notices filed after the notice `after`
|
|
1893
1867
|
(all of them without it, at most 500), oldest first, as `notice` events on this connection, then each
|
|
1894
1868
|
new one as it is filed; it returns `{replayed}`. `notice.list {lines?, limit?}` returns the
|
|
1895
|
-
unacknowledged notices, newest first (default 50), and `counts`:
|
|
1896
|
-
yet marked read over every conversation, and `requests`, the asks, message requests and suggestions held. `notice.ack {ids}` marks
|
|
1869
|
+
unacknowledged notices, newest first (default 50), and `counts.requests`: the asks, message requests and suggestions held. `notice.ack {ids}` marks
|
|
1897
1870
|
notices seen; unknown ids are ignored. It never answers, releases or uncounts a request: a request stays held, in
|
|
1898
1871
|
`request.list` and `counts.requests`, until `request.answer` (or its expiry). Notices go with the transcript's retention, and acknowledged ones
|
|
1899
1872
|
with the read mail's.
|
|
@@ -1928,8 +1901,7 @@ delegation chain is verified after these (§6.7). A person refuses another perso
|
|
|
1928
1901
|
- Receipts, `task.status` and `task.artifact` answer the recipient's own traffic, so they have a
|
|
1929
1902
|
budget of their own: four times that.
|
|
1930
1903
|
- Presence has a budget of its own, also four times that.
|
|
1931
|
-
-
|
|
1932
|
-
`LIMITS.inboundPerMachinePerMinute`, apart from agent mail.
|
|
1904
|
+
- Person-addressed controls (contact requests, cards and grants) have their own budget of `LIMITS.inboundPerMachinePerMinute`.
|
|
1933
1905
|
- Over the limit, an envelope is **deferred**: the recipient withholds its `recv.ack` and does not
|
|
1934
1906
|
mark it seen, so the relay sends it again later (§7 "Delivery") and it is taken when the budget
|
|
1935
1907
|
has room. A backlog that arrives at once is so taken at the budget's pace, all of it, not
|
|
@@ -1938,9 +1910,7 @@ delegation chain is verified after these (§6.7). A person refuses another perso
|
|
|
1938
1910
|
can wait.
|
|
1939
1911
|
- An envelope over the limit that would expire before the budget has room for it, with a minute
|
|
1940
1912
|
to spare for the relay's next redelivery, can never be taken. It is dropped (acked). A message
|
|
1941
|
-
sender gets at most one `rate_limited` receipt a minute.
|
|
1942
|
-
with `rate_limited` for every such chat envelope, up to four times the chat budget a minute, so
|
|
1943
|
-
its line is `refused` at once instead of `sent` until it expires.
|
|
1913
|
+
sender gets at most one `rate_limited` receipt a minute. Person-addressed controls get individual refusals, bounded by the response budget.
|
|
1944
1914
|
- Presence over its budget is dropped: it is never queued, and the next one supersedes it.
|
|
1945
1915
|
- **Quota.** A recipient may cap the inbox items and bytes it holds from one sender machine
|
|
1946
1916
|
(`rate_limited` past it).
|
|
@@ -1953,6 +1923,21 @@ delegation chain is verified after these (§6.7). A person refuses another perso
|
|
|
1953
1923
|
- **`wake` on an inbox item.** This is the sender's hint, AND-ed with the session's tier, which must be
|
|
1954
1924
|
`push`. Only what consent delivers becomes an inbox item, so a held request never wakes a session.
|
|
1955
1925
|
|
|
1926
|
+
- **Mixed versions** (0.7.1 and the conversations release). Neither side shims the other, and neither leaves a
|
|
1927
|
+
half-open contact:
|
|
1928
|
+
- A daemon from before conversations sends a chat line, plain mail or a message request without a portable proof
|
|
1929
|
+
as an envelope. A current daemon refuses each with a `rejected` receipt, code `refused`, and the `note` "The
|
|
1930
|
+
recipient runs a newer Messaging that takes conversations only: update Messaging to reach them." 0.7.1 records
|
|
1931
|
+
only the code: it shows `refused` with its hint "do not retry; tell your person", the clearest it can show, since
|
|
1932
|
+
none of its hints names an update. The note is for later senders and the logs.
|
|
1933
|
+
- A current daemon asks the relay (`conv.caps`, §7) before it creates a dm, group or channel with a person, adds
|
|
1934
|
+
one, or sends a message request. A person whose known devices all lack the `conversations` presence feature is
|
|
1935
|
+
refused with `peer_outdated`, "<handle> needs to update Messaging", and nothing is created or sent. With no
|
|
1936
|
+
presence for that person in the envelope TTL, the relay has no evidence, and the call goes ahead.
|
|
1937
|
+
- Tasks keep their envelopes and work in both directions.
|
|
1938
|
+
- Rollout: the relay first (it serves 0.7.1 unchanged), then every daemon of a realm together
|
|
1939
|
+
(`docs/distribution.md`, "Versions and compatibility").
|
|
1940
|
+
|
|
1956
1941
|
### 8.4 Delivery stages (sender view)
|
|
1957
1942
|
|
|
1958
1943
|
- The normal sequence is `accepted → relayed → delivered → handed_to_agent → read`. A request the
|
|
@@ -2035,8 +2020,8 @@ then lives in plaintext in the sender's and recipient's stores for their retenti
|
|
|
2035
2020
|
and in the recipient's model context. The sending daemon therefore refuses such content before it
|
|
2036
2021
|
stores or seals anything. The relay never sees plaintext, so this check exists only on the sender.
|
|
2037
2022
|
|
|
2038
|
-
- **Where.** `
|
|
2039
|
-
and artifact), `task.cancel` (`reason`)
|
|
2023
|
+
- **Where.** `task.create`, `task.continue`, `conv.send`, `conv.edit`, `task.update` (status message
|
|
2024
|
+
and artifact), and `task.cancel` (`reason`). The daemon reads every string in
|
|
2040
2025
|
the call's params except the session `key`: values and object keys at any depth, including
|
|
2041
2026
|
`metadata` and `data` parts, and the base64-decoded bytes of every `raw` field, read as UTF-8.
|
|
2042
2027
|
- **When.** After the key and task-role checks (`not_found`, `invalid_state` for a wrong role), and
|
|
@@ -2075,29 +2060,62 @@ stores or seals anything. The relay never sees plaintext, so this check exists o
|
|
|
2075
2060
|
|
|
2076
2061
|
### 8.8 Store schema
|
|
2077
2062
|
|
|
2078
|
-
The daemon's store (`messaging.db`) and the relay's database each have one baseline, schema 1
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
- **
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2063
|
+
The daemon's store (`messaging.db`) and the relay's database each have one baseline, schema 1, as release 0.7.0
|
|
2064
|
+
created it (owner, 2026-11-08):
|
|
2065
|
+
|
|
2066
|
+
- **Release 0.7.0 was the last store reset.** It reads no store of 0.1.0 to 0.6.0. It is the first release another
|
|
2067
|
+
person uses (Jelly's hosted users), so no later release resets a store or raises a generation.
|
|
2068
|
+
- **From 0.7.0 on, stores only grow**, so that a host can roll a daemon or relay back by moving the binary back
|
|
2069
|
+
and keeping the data. A store change adds an idempotent migration and a new schema number, under the rules of
|
|
2070
|
+
"When stores grow" below; the baseline is never edited.
|
|
2071
|
+
- The daemon store is at **schema 4** and the relay database at **schema 3**, each the baseline plus its migrations:
|
|
2072
|
+
- daemon schema 2 adds `conv_logs`, `conv_entries`, `conv_shares`, `conv_cursors`, `conv_outbox`, `conv_keys`,
|
|
2073
|
+
`conv_alarms` and `conv_fts`; schema 3 adds `conv_held`; schema 4 adds `conv_evidence`;
|
|
2074
|
+
- relay schema 2 adds `conv_logs`, `conv_entries`, `conv_members` and `conv_shares` (§7 "Conversation frames");
|
|
2075
|
+
schema 3 adds `conv_request_decisions`.
|
|
2076
|
+
Each keeps `min_reader` 1, so a 0.7.x daemon or relay still opens the store after a rollback and ignores the new
|
|
2077
|
+
tables.
|
|
2086
2078
|
|
|
2087
2079
|
**Records.** The daemon's store keeps `generation`, `schema` and `min_reader`, the lowest schema that can read
|
|
2088
2080
|
it, in its `kv` table. The relay keeps `generation` and `min_reader` in its `meta` table, and `schema` in
|
|
2089
|
-
`PRAGMA user_version`.
|
|
2090
|
-
0.6.0 have no generation (generation 1).
|
|
2081
|
+
`PRAGMA user_version`. Every store from 0.7.0 on records generation 2 and `min_reader` 1; a new daemon store records
|
|
2082
|
+
schema 4, and a new relay database schema 3. The stores of 0.1.0 to 0.6.0 have no generation (generation 1).
|
|
2091
2083
|
|
|
2092
|
-
**Opening.** A store with no tables is new: the daemon or relay writes the baseline and
|
|
2084
|
+
**Opening.** A store with no tables is new: the daemon or relay writes the baseline, its records and every migration. For the relay,
|
|
2093
2085
|
the tables Litestream adds when it replicates the database (`_litestream_*`) do not count. It opens
|
|
2094
2086
|
any other store only when the store records its own generation and a `min_reader` at most its own schema. It
|
|
2095
|
-
refuses every other store and changes nothing in it
|
|
2096
|
-
relay, move the home (or the relay database,
|
|
2097
|
-
|
|
2098
|
-
daemon checks the store before it reads or writes anything else in its home. Nothing deletes a home or
|
|
2087
|
+
refuses every other store and changes nothing in it. The error gives the reason and its remedy (below).
|
|
2088
|
+
Only `pre-reset` calls for starting over: stop the daemon or relay, move the home (or the relay database,
|
|
2089
|
+
with its `-wal` and `-shm` files) aside, and set it up again. For `newer`, upgrade the reader and keep the data.
|
|
2090
|
+
A daemon checks the store before it reads or writes anything else in its home. Nothing deletes a home or
|
|
2099
2091
|
database by itself.
|
|
2100
2092
|
|
|
2093
|
+
**A refused store** is machine-readable, so that a client never matches the wording. Every refusal has one of two
|
|
2094
|
+
**reasons**, and the remedy depends on it:
|
|
2095
|
+
|
|
2096
|
+
| Reason | The store | Remedy |
|
|
2097
|
+
|---|---|---|
|
|
2098
|
+
| `pre-reset` | records no store generation, or another generation than the reader's | Start over: move the home (or the relay database) aside and set it up again. |
|
|
2099
|
+
| `newer` | its `min_reader` is above the reader's schema: a newer messagingd or relay wrote it | Upgrade the daemon or relay, and keep the home or database as it is. Never move it aside for this reason. |
|
|
2100
|
+
|
|
2101
|
+
- `messagingd` exits with status **78** (`EX_CONFIG` of sysexits(3); `STORE_REFUSED_EXIT` in `@messaging/core`)
|
|
2102
|
+
for either reason, and prints one line on stderr, `messagingd: store_refused: <reason>: <why, and what to do>`.
|
|
2103
|
+
No other failure exits 78.
|
|
2104
|
+
- `messaging-relay` exits 78 and logs one JSON event, `relay.store_refused {db, reason, message}`: `reason` is
|
|
2105
|
+
`pre-reset` or `newer`, and `message` is the text.
|
|
2106
|
+
- A client that starts the daemon and sees it exit 78 reports the error code `store_refused` (§13), with the reason
|
|
2107
|
+
in a field of its own (`StoreRefusedError.reason`; `reason` in `messaging --json` errors and in agent tool
|
|
2108
|
+
errors). It reads the reason from the two tokens after `messagingd:` on the line that start printed
|
|
2109
|
+
(`storeRefusedReason` in `@messaging/core`) and never parses the text after them, which may change between
|
|
2110
|
+
releases. A line without a reason token gives `store_refused` without a reason. `messaging daemon start` does
|
|
2111
|
+
this.
|
|
2112
|
+
- A client offers to start over (moving the home aside) **only for `pre-reset`**. For `newer`, and for a refusal
|
|
2113
|
+
without a reason, it tells the person to upgrade or to read messagingd's line, and moves nothing.
|
|
2114
|
+
- A home moved aside is named `<home>.pre-reset-<UTC timestamp>`, the timestamp as `YYYYMMDDTHHMMSSZ` (for example
|
|
2115
|
+
`~/.messaging.pre-reset-20261108T171500Z`). The name carries no version, so it holds for every reset, and a
|
|
2116
|
+
client that denies its agents the home also denies `<home>.pre-reset-*`: the moved home keeps the old keys and
|
|
2117
|
+
messages. The messagingd that wrote it still opens it.
|
|
2118
|
+
|
|
2101
2119
|
**When stores grow.**
|
|
2102
2120
|
|
|
2103
2121
|
- A daemon or relay opens any store of its generation whose `min_reader` is at most its own schema. It
|
|
@@ -2111,10 +2129,13 @@ database by itself.
|
|
|
2111
2129
|
- A breaking change needs a new store file with an explicit export and import, never an in-place change.
|
|
2112
2130
|
`min_reader` is raised only in an emergency, and hosts that pin the daemon are told first.
|
|
2113
2131
|
|
|
2114
|
-
**The test.** `store-schemas
|
|
2115
|
-
(tables, columns, indexes, constraints, triggers)
|
|
2116
|
-
|
|
2117
|
-
|
|
2132
|
+
**The test.** `store-schemas/<n>.json` in `packages/daemon` and `packages/relay` is the shape of schema n as
|
|
2133
|
+
released (tables, columns, indexes, constraints, triggers); `1.json` is the baseline as 0.7.0 created it.
|
|
2134
|
+
`test/release/store-schema.test.ts` in each package holds the baseline to `1.json` and HEAD's new store to the last
|
|
2135
|
+
snapshot, so a store change is seen in review. Each also opens a database the schema-1 release itself wrote
|
|
2136
|
+
(`test/fixtures/relay-schema-1.sql` and `test/fixtures/daemon-schema-1.sql`), checks that it migrates in place with every row intact except the schema marker, that migrating again
|
|
2137
|
+
changes nothing, and that a schema-1 reader still opens the result: its records allow it, and every table, column
|
|
2138
|
+
and index of schema 1 is unchanged inside it.
|
|
2118
2139
|
|
|
2119
2140
|
The wire rule (§14) is separate; a rolled-back daemon must still speak to newer peers within `messaging/1`.
|
|
2120
2141
|
|
|
@@ -2231,9 +2252,9 @@ and names each session an engine may register.
|
|
|
2231
2252
|
- `exp - iat` is in (0, 60] seconds, `iat` at most 5 s ahead of the daemon's clock and `exp` at most 5 s
|
|
2232
2253
|
behind it;
|
|
2233
2254
|
- `cls` is required, and is the method's action class (`ACTION_CLASSES`, `actionClass` in `@messaging/core`):
|
|
2234
|
-
`chat` for `
|
|
2235
|
-
read-only human-tier methods and `device.list` (`conv.list`, `conv.read`, `conv.
|
|
2236
|
-
`people.get`, `grant.list`, `request.list`, `
|
|
2255
|
+
`chat` for `conv.send`, `conv.edit`, `conv.delete`, `conv.react` and `notice.ack`; `view` for the reads a host's section makes, the
|
|
2256
|
+
read-only human-tier methods and `device.list` (`conv.list`, `conv.read`, `conv.sync`, `conv.get`, `conv.members`, `conv.threads`, `conv.search`, `conv.context`, `conv.access`, `request.waiting`, `people.search`,
|
|
2257
|
+
`people.get`, `grant.list`, `request.list`, `sessions.list`, `notice.subscribe`, `notice.list`,
|
|
2237
2258
|
`device.list`); `local` for every other method, the previews and every other admin-tier method included.
|
|
2238
2259
|
(`device.status` is agent tier: it needs no grant.) So `view` is refused on any method that writes, and a preview, which tells what
|
|
2239
2260
|
a `local` action would do, takes `local`. A plugin's `jelly` declaration must give each action and read the same
|
|
@@ -2247,8 +2268,8 @@ and names each session an engine may register.
|
|
|
2247
2268
|
`person.attached`, `daemon.snapshot`, `wake.arm` and `wake.disarm`.
|
|
2248
2269
|
It is bound to the first connection that presents it, and may be presented again on that connection until
|
|
2249
2270
|
it expires; on another connection, or after a restart, it is `grant_invalid`. Under a host-scope grant
|
|
2250
|
-
`conv.list`
|
|
2251
|
-
|
|
2271
|
+
`conv.list` is refused before content is read. Lane 4 exposes no placeholder counts.
|
|
2272
|
+
A `notice.subscribe` under a host-scope grant ends when the grant does: the
|
|
2252
2273
|
host subscribes again with a fresh grant and `after`.
|
|
2253
2274
|
- **Refusals.** No grant is `grant_required`; any failure above is `grant_invalid`. The agent tier needs no
|
|
2254
2275
|
grant on either socket. Both are checked before the params are read. The daemon logs each refusal with
|
|
@@ -2319,6 +2340,10 @@ callers rely on.
|
|
|
2319
2340
|
It answers from what this machine has verified, synchronously; a caller catches up before it
|
|
2320
2341
|
verifies. `Roster` implements it: `realm` is the team id, `device` is `member`, `anchor()` is
|
|
2321
2342
|
`rosterAnchor`.
|
|
2343
|
+
- Conversations (§16): `checkVia`, `currentLinks`, `signConvEvent`, `verifyConvEvent`, `sealConvMsg`, `openConvMsg`, `sealConvMeta`,
|
|
2344
|
+
`openConvMeta`, `sealKeyShares`, `openKeyShares`, `ConvLog.apply`, `ConvLog.applyExpired`, `ConvLog.relate`
|
|
2345
|
+
and `resolveContext` throw only `MessagingError`. `ConvLog.at(seq)` is the state a log holding only entries 1
|
|
2346
|
+
to `seq` has. `ReceiverChains.messageKey` hands out each message key once.
|
|
2322
2347
|
- `createGrant` narrows its parent and never widens it (`forbidden`); an expired parent is `expired`.
|
|
2323
2348
|
- Consent (§6.9): `signConsentGrant` refuses a grant that breaks a rule (`invalid_request`) or is
|
|
2324
2349
|
over `LIMITS.grantBytes` (`too_large`); `verifyConsentGrant` checks the schema, the rules, the
|
|
@@ -2470,30 +2495,38 @@ daemon owned by the uid in `MESSAGING_DAEMON_UID` (§8.1).
|
|
|
2470
2495
|
|
|
2471
2496
|
### Agent tools
|
|
2472
2497
|
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
`inbox`, `read`, `search`, `tasks`, `task_update` and `task_cancel`.
|
|
2498
|
+
**Conversation tools.** `ConversationTools` is the shared schema registry for the conversation-model
|
|
2499
|
+
cut-over. Its ten verbs are `contacts`,
|
|
2500
|
+
`conversations`, `send`, `inbox`, `read`, `search`, `context`, `tasks`, `task_update` and `task_cancel`.
|
|
2501
|
+
`conversations` uses `conv.access`; `send`, `read`, `search` and `context` use their `conv.*` counterparts.
|
|
2502
|
+
Results identify a line by the stable citation `messaging:<cid>/<id>`. The client fences peer text and names,
|
|
2503
|
+
neutralizes fence closers, and labels own text as ours. List results contain at most 50 events and 32 KiB of text;
|
|
2504
|
+
each fenced event also has the existing per-part cap, `LIMITS.toolTextChars` (32,768).
|
|
2505
|
+
Long bodies have `[truncated: N bytes; read the item for the full text]`, where N counts omitted UTF-8 bytes.
|
|
2506
|
+
The stored body stays intact. `read({source, offset})` continues at the returned `nextOffset` until the full body is shown.
|
|
2507
|
+
A held result waits for consent and does not disclose the person's answer. Tools grant no human-tier action.
|
|
2508
|
+
|
|
2509
|
+
The OpenCode plugin and `messaging-mcp` use the same ten-tool contract in `@messaging/core`, normative like `schema/**`.
|
|
2510
|
+
Their names are
|
|
2476
2511
|
`messaging_<verb>`; in OpenCode's Code Mode, `tools.messaging.<verb>`. No tool grants, adds
|
|
2477
2512
|
contacts, changes devices or administers a team or channel.
|
|
2478
2513
|
|
|
2479
|
-
- **Addresses.** `
|
|
2480
|
-
|
|
2481
|
-
agent
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
`
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
first, with opaque `before`/`after` cursors. Mail and task entries carry previews; outgoing mail
|
|
2492
|
-
carries no text, because the daemon keeps no copy of what it sent.
|
|
2514
|
+
- **Addresses.** Lines accept a canonical `cid` or a person or agent destination.
|
|
2515
|
+
`#<channel>` resolves through accessible conversations. An agent sends to a conversation it is seated in, or to another
|
|
2516
|
+
agent (`person/agent`). With no dm yet, its person's device signs the create of the agent dm, whose only participants
|
|
2517
|
+
are the two agents; the recipient's `ask` admits the invited agent (`open` at once, `request` after its person
|
|
2518
|
+
accepts, `no` never, silently). Until the seat the line waits on the sending device, keeps the id the send returned,
|
|
2519
|
+
shows in `request.waiting` as `pending-delivery`, and lapses after 7 days. An agent cannot start a dm with a person,
|
|
2520
|
+
and cannot add or seat anyone. The sender-side consent is the client's first-send confirmation for the address (the
|
|
2521
|
+
plugin's `messaging.send` permission), as for 0.7.1 mail; the daemon has no separate signal.
|
|
2522
|
+
Retained tasks use `person/agent[@label]`. Malformed addresses are `invalid_request`.
|
|
2523
|
+
An ambiguous task address requires a device label. Human contact requests remain human-only.
|
|
2524
|
+
- **Conversations.** `read({cid})` pages current verified events with opaque cursors.
|
|
2525
|
+
Conversation identity is stable across devices. Legacy room names are not aliases.
|
|
2493
2526
|
- **`contacts`** lists `me` (address, person, agent, device label, relay `connected` or `offline`), the
|
|
2494
|
-
persons in the directory and in
|
|
2495
|
-
`contacts.list` (§8.2), repo, branch and doing), and
|
|
2496
|
-
|
|
2527
|
+
persons in the directory and in conversations, each with their agents (state, `expect` from
|
|
2528
|
+
`contacts.list` (§8.2), repo, branch and doing), and channels allowed by `conv.access`.
|
|
2529
|
+
Counts and read preferences are omitted when unavailable. A person's `can` is the daemon's `reach` for them (§8.2
|
|
2497
2530
|
`contacts.list`): `source: "card"` with `card`, when their person signed it, or `source: "default"`, the
|
|
2498
2531
|
defaults for the org or for a stranger; `can.chat` is false for one's own person and when the client offers no
|
|
2499
2532
|
DMs. An agent's `can` is its person's, except that `can.chat` is false and the inbound policy narrows it (§8.3: a
|
|
@@ -2503,28 +2536,21 @@ contacts, changes devices or administers a team or channel.
|
|
|
2503
2536
|
refused. `can: "ask"` lists those whose `can.ask` is not `no`. A channel's `can` is `default`. `presence` is
|
|
2504
2537
|
`online`,
|
|
2505
2538
|
`offline` or (for a channel) `unknown`. No field is invented.
|
|
2506
|
-
-
|
|
2507
|
-
task
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
fixed `note`. A result at stage `held` (the recipient's person answers a request card first, §6.9)
|
|
2514
|
-
carries `expect: "on_consent"` and a note saying so. `request: {contact: <email>}`, only with `to: "me"`
|
|
2515
|
-
(otherwise `invalid_request`), passes `msg.send {request}`: the line goes to the agent's own person with a
|
|
2516
|
-
suggestion to send a message request to that exact email (§6.9, "Message requests"); the agent cannot send one,
|
|
2517
|
-
and its note says it is not told what the person decides. A send refused `request_required` tells the agent to
|
|
2518
|
-
ask its person this way.
|
|
2539
|
+
- **Sources.** An item target or result names `source: {kind: "conv", cid, event}` or `source: {kind: "task", taskId}`.
|
|
2540
|
+
A task never receives a fabricated conversation id. Conversation item identity includes both `cid` and `event`.
|
|
2541
|
+
- **`send`** uses `conv.send` for lines, including agent DMs. `ask` uses `task.create`.
|
|
2542
|
+
A task continuation uses `task.continue`. Task sends retain `files`, `replyTo`, `onBehalfOf` and `key`.
|
|
2543
|
+
Delegation requires an unfinished task that this session executes. `key` or `clientId` makes retries idempotent.
|
|
2544
|
+
A line returns its conversation source and `pending`. A task returns its task source and delivery results.
|
|
2545
|
+
An authenticated hold carries `expect: "on_consent"`. Missing receipts do not imply a hold.
|
|
2519
2546
|
- **`inbox`** lists mail and task requests (`inbox.list`), unread by default, and acks
|
|
2520
2547
|
`handed_to_agent` for the previews it shows. `consent` says how a request from the sender runs
|
|
2521
2548
|
here: `self` (the session's own person), `trusted` or `guarded`. It is the inbox item's `consent`, the
|
|
2522
2549
|
daemon's decision (§6.9); a client decides it only for a task record or an item stored before
|
|
2523
2550
|
consent, which carry none.
|
|
2524
|
-
- **`read({
|
|
2525
|
-
|
|
2526
|
-
- **`search`**
|
|
2527
|
-
searched.
|
|
2551
|
+
- **`read({source})`** reads the named conversation event or retained task. It preserves the daemon's consent decision.
|
|
2552
|
+
A held event read acknowledges nothing. `read({cid})` returns a conversation page with opaque cursors.
|
|
2553
|
+
- **`search`** uses `conv.search` within the daemon's access rules.
|
|
2528
2554
|
- **`tasks`** lists `task.list` with roles `mine` (requester) and `asked` (executor) and short state
|
|
2529
2555
|
names (`working`, `input_required`, …); `state` also takes `open` and `done`.
|
|
2530
2556
|
- **`task_update`** (executor) and **`task_cancel`** (requester) map to `task.update` and
|
|
@@ -2563,12 +2589,12 @@ contacts, changes devices or administers a team or channel.
|
|
|
2563
2589
|
|
|
2564
2590
|
### Clients
|
|
2565
2591
|
|
|
2566
|
-
- **OpenCode plugin.** Tools under namespace `messaging`: `contacts`, `send`, `inbox`, `read`,
|
|
2567
|
-
`search`, `tasks`, `task_update` and `task_cancel`. Every send asserts `messaging.send` with the
|
|
2592
|
+
- **OpenCode plugin.** Tools under namespace `messaging`: `contacts`, `conversations`, `send`, `inbox`, `read`,
|
|
2593
|
+
`search`, `context`, `tasks`, `task_update` and `task_cancel`. Every send asserts `messaging.send` with the
|
|
2568
2594
|
address as its resource (`person/agent`, `#channel`, or `person` and `person/`), and every read of
|
|
2569
2595
|
a conversation asserts `messaging.read` with the conversation (a `person:` one asks first by
|
|
2570
2596
|
default). Inbound content is framed as untrusted data. Details are in the plugin's repository, `joe-broadhead/opencode-messaging` (`docs/`).
|
|
2571
|
-
- It never
|
|
2597
|
+
- It never changes human read state.
|
|
2572
2598
|
- With wakes on, it registers each session with `host: {kind: "opencode", location, sessionID,
|
|
2573
2599
|
pid}`: its location directory, the OpenCode session id, and the server's pid.
|
|
2574
2600
|
- Its RPC `messaging.opencode/wake {sessionID, envelopeIds}` serves the wake bridge (§8.6). It checks
|
|
@@ -2592,16 +2618,10 @@ contacts, changes devices or administers a team or channel.
|
|
|
2592
2618
|
item only after the host recorded the tool result with the note intact; until then the item is
|
|
2593
2619
|
only offered, and an offer that fails or is cut goes back to waiting. No wire change.
|
|
2594
2620
|
- **MCP server.** `messaging-mcp` (stdio) for other agents. It registers one `pull`-tier session and
|
|
2595
|
-
subscribes to its events.
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
- MCP has no per-conversation approval of its own, so chats are opt-in: the user names channels
|
|
2600
|
-
when configuring the server, with `--channel <name>` (repeatable). `send`, `read` and `search`
|
|
2601
|
-
reach those channels only, and are refused (`forbidden`) for any other channel before any daemon
|
|
2602
|
-
call. Conversations with persons (DM lines) are never available over MCP: a line to a person, or
|
|
2603
|
-
`read`/`search` with one, is `forbidden`. It never calls `conv.subscribe`, `conv.markRead`,
|
|
2604
|
-
`person.send` or `channel.*`.
|
|
2621
|
+
subscribes to its events. It registers the ten tools in `ConversationTools`: `messaging_contacts`, `messaging_conversations`, `messaging_send`, `messaging_inbox`, `messaging_read`, `messaging_search`, `messaging_context`, `messaging_tasks`, `messaging_task_update` and `messaging_task_cancel`.
|
|
2622
|
+
Lines and conversation reads use `conv.*`. Retained tasks use `task.create/continue/list/get/update/cancel` and `inbox.list/get/ack`.
|
|
2623
|
+
- The daemon controls access through `conv.access`. Contacts expose only accessible channel cards.
|
|
2624
|
+
MCP uses the agent socket and never calls human-tier methods or changes human read state.
|
|
2605
2625
|
- It has no peer-turn guard: its agent reads every request itself, with its own tools. It reports
|
|
2606
2626
|
the daemon's `consent` for each item; where the daemon gives none (a task record), `self` for the
|
|
2607
2627
|
session's own person and `trusted` for everyone else.
|
|
@@ -2738,7 +2758,7 @@ contacts, changes devices or administers a team or channel.
|
|
|
2738
2758
|
`prompt` when a dialog on every daemon start is acceptable.
|
|
2739
2759
|
- **Local IPC.** The sockets trust users, not processes (§8.2).
|
|
2740
2760
|
- The daemon enforces two sockets: `run/agent.sock` serves only `AGENT_METHODS`, so never a line as
|
|
2741
|
-
the person
|
|
2761
|
+
the person; `run/admin.sock` serves everything. Agent conversation calls require a registered session.
|
|
2742
2762
|
- Under one OS user that is no boundary: any process of the user can open `run/admin.sock`, call
|
|
2743
2763
|
`team.*` and `daemon.stop`, and act for any session whose key it knows: session keys are bearer
|
|
2744
2764
|
credentials. The tiers keep those methods off agent tool paths, nothing more.
|
|
@@ -2783,6 +2803,10 @@ contacts, changes devices or administers a team or channel.
|
|
|
2783
2803
|
|
|
2784
2804
|
## 13. Error codes
|
|
2785
2805
|
|
|
2806
|
+
Tool errors use the codes in this section for both conversation and task sources (§11).
|
|
2807
|
+
An unknown source is `not_found`. A source that combines task and conversation identifiers is `invalid_request`.
|
|
2808
|
+
Oversized model-visible text is truncated with the explicit marker in §11, rather than reported as `too_large`.
|
|
2809
|
+
|
|
2786
2810
|
| Code | Meaning |
|
|
2787
2811
|
|---|---|
|
|
2788
2812
|
| invalid_request | malformed input or schema violation |
|
|
@@ -2795,6 +2819,7 @@ contacts, changes devices or administers a team or channel.
|
|
|
2795
2819
|
| refused | recipient policy refuses this sender |
|
|
2796
2820
|
| declined | the recipient's person declined a held request (§6.9) |
|
|
2797
2821
|
| request_required | the recipient's person has not accepted this sender: another org's person needs a message request first (§6.9) |
|
|
2822
|
+
| peer_outdated | the other person's devices all run a Messaging from before conversations (`conv.caps`, §7); nothing was created or sent. They need to update first |
|
|
2798
2823
|
| rate_limited | too many envelopes; retry later |
|
|
2799
2824
|
| loop_guard | agent-to-agent loop guard tripped |
|
|
2800
2825
|
| too_large | size limit exceeded |
|
|
@@ -2808,10 +2833,12 @@ contacts, changes devices or administers a team or channel.
|
|
|
2808
2833
|
| guarded_turn | a guarded peer turn tried to send to anyone but the person who started it (§11; a client code, never on the wire) |
|
|
2809
2834
|
| roster_conflict | roster entry does not extend the current head |
|
|
2810
2835
|
| invite_invalid | invite unknown, expired, revoked, used or mismatched |
|
|
2811
|
-
| no_identity | the daemon has
|
|
2836
|
+
| no_identity | the daemon has no usable identity: no team, or a directory device not yet bound with published keys (§15.9) |
|
|
2812
2837
|
| binding_invalid | a device binding, a device-log entry or its authority failed verification (§15) |
|
|
2813
|
-
| jelly_unavailable | Jelly ID
|
|
2838
|
+
| jelly_unavailable | Jelly ID could not be reached or returned an unusable response during key fetching (§15.2) or device sign-in (§15.9); never reported as `binding_invalid` |
|
|
2814
2839
|
| log_conflict | a device-log entry does not extend the log's head (§15.3); resync and retry |
|
|
2840
|
+
| conv_conflict | a conversation entry does not extend the log, or an event's op head is not the current op (§16.7); re-read and retry |
|
|
2841
|
+
| stale_epoch | a conversation message is sealed under an epoch that is not the current one, or a rekey is due (§16.7); re-seal and retry |
|
|
2815
2842
|
| device_limit | the person already has `LIMITS.devicesPerPerson` current devices (§15.3) |
|
|
2816
2843
|
| admission_required | `dir.bind` of an entry that is not a device binding itself with a `jelly` authority (§15.10) |
|
|
2817
2844
|
| admission_invalid | `dir.bind` whose Jelly token is not live now, or not for this directory's issuer, resource and person (§15.10) |
|
|
@@ -2826,6 +2853,7 @@ contacts, changes devices or administers a team or channel.
|
|
|
2826
2853
|
| ticket_invalid | `deployment: "jelly"`: the session ticket did not verify, is out of its lifetime, or names another session or agent (§8.11) |
|
|
2827
2854
|
| daemon_unavailable | a client cannot reach the daemon |
|
|
2828
2855
|
| daemon_mismatch | the daemon on the socket is another release or IPC protocol than the client, or did not say which (§8.2) |
|
|
2856
|
+
| store_refused | messagingd refused the home's store and changed nothing; it exits 78 (§8.8). Its `reason` field says why: `pre-reset` (no or another store generation: start over by moving the home aside) or `newer` (a `min_reader` above its schema: upgrade, keep the home); absent when messagingd named none. A client code: a client that starts the daemon reports it, never the daemon over IPC |
|
|
2829
2857
|
| internal | unexpected failure (details stay in local logs) |
|
|
2830
2858
|
|
|
2831
2859
|
## 14. Versioning
|
|
@@ -3327,11 +3355,9 @@ when a restore does not reach it: keep it on other storage than the database, or
|
|
|
3327
3355
|
who accepts the fork on purpose moves the witness aside and starts again.
|
|
3328
3356
|
|
|
3329
3357
|
**Resolution.** From each person's newest verified Jelly token the directory keeps their name, their email when
|
|
3330
|
-
Jelly ID proved it, and from that email their **handle** and **org** (`
|
|
3358
|
+
Jelly ID proved it, and from that email their **handle** and **org** (`hostedHandle`, `emailOrg`):
|
|
3331
3359
|
|
|
3332
|
-
- the handle
|
|
3333
|
-
trimmed from both ends, cut to 30 characters (trimmed again), and `u-` put in front when it does not start with
|
|
3334
|
-
a letter;
|
|
3360
|
+
- the handle uses `hostedHandle`, exactly as §15.8 defines it;
|
|
3335
3361
|
- the org is `domain:<d>` when the email's domain `d` is in the operator's `orgDomains` (`--org-domains`), and
|
|
3336
3362
|
none otherwise: such a person is an individual, in no org.
|
|
3337
3363
|
|
|
@@ -3424,3 +3450,432 @@ five minutes, until it answers, so a new directory becomes ready without waiting
|
|
|
3424
3450
|
`dir_wakes`: each Jelly host's device's armed wake ticket, its `agents` and `awaiting`, its `exp` and when it may
|
|
3425
3451
|
next be presented (§7, "Wake tickets"). At start the directory rebuilds the map and the epoch log from them
|
|
3426
3452
|
and refuses to start if they disagree with the last tree head.
|
|
3453
|
+
|
|
3454
|
+
## 16. Conversations
|
|
3455
|
+
|
|
3456
|
+
**Guest acceptance (X3-1).** An invited participant is not a content reader.
|
|
3457
|
+
The relay serves authenticated control history and heads to invitees, with message digests instead of ciphertext and a zero share cursor.
|
|
3458
|
+
It pushes invitation operations, never message content or keys, to a person represented only by invited seats.
|
|
3459
|
+
`ConvLog.checkShare` requires an accepted recipient. For a group or history `none`, acceptance (including seating an invited
|
|
3460
|
+
agent) is the effective addition: it updates `since` and requires an `added` rekey before messages or shares.
|
|
3461
|
+
Inviting alone does not start that epoch. Shared channels keep their declared shared-history exposure after acceptance.
|
|
3462
|
+
A guest cannot invite or change to another role, even via an intermediate member role. Agents inherit their owner's
|
|
3463
|
+
guest restriction; a guest agent needs an accepted, visible guest owner and that owner's signed seat. Live org checks
|
|
3464
|
+
cover agent principals too; two persons without an org are not implicitly one org.
|
|
3465
|
+
|
|
3466
|
+
A guest's `accept` carries `invitation`, a `GuestInvitation` signed under `DOMAIN.guestInvitation`.
|
|
3467
|
+
The preview binds the invitation ID, inviter device, realm, conversation, invitee, exact op head, expiry, current audience and exposure policies.
|
|
3468
|
+
The device that signed the original add or create signs this bounded preview. The inviter seals it to the invitee without history or keys.
|
|
3469
|
+
The accept event signs the same preview. A changed audience, policy or op head requires a fresh preview.
|
|
3470
|
+
After acceptance, the invitee fetches the log and verifies the invitation independently before content or key use.
|
|
3471
|
+
`guestHistoryExposure` supplies explicit text for shared history and history from the acceptance boundary.
|
|
3472
|
+
|
|
3473
|
+
**Agent-DM admission.** The recipient's `ask` policy controls an agent-only DM invitation, including invitations across orgs.
|
|
3474
|
+
`open` seats the recipient agent, `request` holds the invitation for the person, and `no` silently declines it locally.
|
|
3475
|
+
`request.list` exposes a held invitation as `{kind:"agent-invitation",id,cid,from,to,at}`, without content or keys.
|
|
3476
|
+
`request.answer` accepts `accept` or `decline`. Its preview identifies the sender and target agent and reports the affected count.
|
|
3477
|
+
With `all`, the answer applies to held agent invitations from the same person.
|
|
3478
|
+
|
|
3479
|
+
The daemon records invitation-only history gaps durably. After acceptance, it fetches those entries again and verifies each signature against its stored digest.
|
|
3480
|
+
It replaces only matching gaps and applies the conversation's history policy before content use.
|
|
3481
|
+
This recovery survives restart. It never restores messages that local retention expired after acceptance.
|
|
3482
|
+
|
|
3483
|
+
A conversation is one hash-chained log of signed events that the relay sequences and members verify. Membership,
|
|
3484
|
+
roles, epochs and settings are a function of the log. Message bodies are sealed under per-device sender keys, so a
|
|
3485
|
+
post is one ciphertext whatever the number of members. The schemas are in `schema/conversation.ts`, the rules in
|
|
3486
|
+
`conversation/` of core, and the vectors in `docs/spec/vectors/conversation.json`. The relay carries the logs in
|
|
3487
|
+
the conversation frames of §7. The daemon's storage and the IPC surface are not specified yet;
|
|
3488
|
+
`docs/design/conversations.md` describes them. Until they land, chats are as §6.8 says.
|
|
3489
|
+
|
|
3490
|
+
### 16.1 Identifiers
|
|
3491
|
+
|
|
3492
|
+
- A **participant** (`ParticipantId`) is a person (§3, §15.1) or one agent of a person: `person/agent`, where
|
|
3493
|
+
`agent` is a session name (§3). Every agent belongs to one person, its **person**.
|
|
3494
|
+
- A conversation id (`ConvId`) is one of:
|
|
3495
|
+
- a **dm**: `"d_" + base32lower(SHA-256("messaging/1/dm\n" || JCS([a, b] sorted)))[0..26]`, where `a` and `b`
|
|
3496
|
+
are its two participants, sorted by UTF-16 code units. Both ends derive the same id. `a` may equal `b`: a
|
|
3497
|
+
person's note to themselves.
|
|
3498
|
+
- a person's **self** conversation: `"s_" + base32lower(SHA-256("messaging/1/self\n" || JCS(person)))[0..26]`.
|
|
3499
|
+
- a **group** or **channel**: `"c_"` and a ULID its creator chooses.
|
|
3500
|
+
|
|
3501
|
+
base32lower is RFC 4648 §6 in lower case without padding, as for `PersonId` (§15.1).
|
|
3502
|
+
- Kinds (`ConvKind`): `dm`, `group`, `channel` and `self`. Only a channel has a visibility (`public` or
|
|
3503
|
+
`private`). The history policy (`none` or `shared`) says whether a late joiner may read earlier messages.
|
|
3504
|
+
- Roles (`ConvRole`): `owner`, `admin`, `member` and `guest`. A participant's state is `member`, or `invited`
|
|
3505
|
+
until it accepts or is seated (§16.7).
|
|
3506
|
+
|
|
3507
|
+
### 16.2 Events and the log
|
|
3508
|
+
|
|
3509
|
+
Every event has a header and a body. The header is:
|
|
3510
|
+
|
|
3511
|
+
```text
|
|
3512
|
+
{v: "messaging/1", realm, cid, id, by: {person, device, agent?}, ts, opHead: {seq, hash}, seen?: {seq, hash},
|
|
3513
|
+
class, epoch?, idx?, wake?}
|
|
3514
|
+
```
|
|
3515
|
+
|
|
3516
|
+
- `id` is a ULID the author chooses. It is unique in the conversation: the log refuses an event whose `id` it
|
|
3517
|
+
already holds (`duplicate`). Other events refer to an event by `id`.
|
|
3518
|
+
- `by` names the signing device, its person and, for an agent's event, the agent. `by.device` must be a usable
|
|
3519
|
+
Ed25519 key (§3).
|
|
3520
|
+
- `opHead` is the last op the author applied: its `seq` and `h(seq)`, or `{0, h(0)}` before any.
|
|
3521
|
+
- `seen` is the head the author had, for gossip (§16.4).
|
|
3522
|
+
- `class` is:
|
|
3523
|
+
- `op`: a control op (`ConvOp`), because the relay routes and checks authority by it. Every piece of state is an
|
|
3524
|
+
op, and ops never expire (§16.4); an op whose content must stay private carries it sealed (§16.5);
|
|
3525
|
+
- `msg`: a sealed `MsgBody`, with the `epoch` and the ratchet index `idx` of its key (§16.5). `wake` (at most
|
|
3526
|
+
`LIMITS.wakeHints` (5)) names sessions of `by.person` the event may wake; it is the only plaintext hint. The
|
|
3527
|
+
author's daemon sets it only on a line that addresses the author's own agents, and the relay acts on it only for
|
|
3528
|
+
the authenticated author's person's devices (§7, "Wake tickets");
|
|
3529
|
+
- `keys`: a sender-key share sealed to one device (§16.6). It is signed like every event but is **not an entry
|
|
3530
|
+
of the log**: the relay keeps it for its recipient alone.
|
|
3531
|
+
- `sig` covers the whole event: `Ed25519(by.device, "messaging/1/conv-event\n" || JCS(event without sig))`.
|
|
3532
|
+
- An event is at most `LIMITS.convEventBytes` (256 KiB) of JCS (`too_large`). Every object is strict: an unknown
|
|
3533
|
+
member is `invalid_request`.
|
|
3534
|
+
|
|
3535
|
+
The relay stores and serves **entries**: `{seq, prev, event}` of ops and msgs, where `seq` counts from 1 and `prev`
|
|
3536
|
+
is `h(seq − 1)`. The relay does not sign entries; members verify the chain (§16.4).
|
|
3537
|
+
|
|
3538
|
+
**Ops** (`ConvOp`): `create`, `add`, `accept`, `join`, `leave`, `remove`, `role`, `seat`, `settings`,
|
|
3539
|
+
`archive`, `rekey`, `policy`, `meta` and `setting`.
|
|
3540
|
+
|
|
3541
|
+
- `create` states the kind, the visibility (channels), the history policy, `retainDays` (1 to
|
|
3542
|
+
`LIMITS.retainDaysMax` (365)), the org a public channel is open to, the participants as seats `{p, role,
|
|
3543
|
+
device?, respondTo?}`, the permits `post`, `invite` and `pins` (`all` or `admins`; absent is `all`), and
|
|
3544
|
+
`sealedMeta` (§16.5).
|
|
3545
|
+
- `add` names 1 to `LIMITS.convOpParticipants` (100) seats; `remove` names as many participants.
|
|
3546
|
+
- `seat` sets an agent participant's `device` (the device that hosts it) and `respondTo` (`self`, `members` or
|
|
3547
|
+
`none`), and accepts its invite. Only an agent's seat carries `device` or `respondTo`.
|
|
3548
|
+
- `settings` changes the visibility, history, retention or permits. `archive {on}` archives or restores.
|
|
3549
|
+
- `rekey {epoch, cause}` starts the next epoch; `cause` is `removed`, `device`, `added`, `settings` or
|
|
3550
|
+
`schedule`. A device rekey also names the removed `device` and may give `until`, a chain position strictly before
|
|
3551
|
+
the rekey. Other causes carry neither field.
|
|
3552
|
+
- `policy {agentContext?, bridges?}` sets one or both, in plaintext (§16.8, §16.10). In a dm or a group it is the
|
|
3553
|
+
author's vote.
|
|
3554
|
+
- `meta {epoch, idx, sealed}`: a group's or channel's `{name?, topic?, purpose?}`, sealed (§16.5).
|
|
3555
|
+
- `setting {kind, epoch, idx, sealed}`: one of the person's settings in their `self` conversation, sealed;
|
|
3556
|
+
`kind` is `prefs`, `grant`, `context`, `localRetention` or `link` (§16.3).
|
|
3557
|
+
|
|
3558
|
+
### 16.3 Message bodies
|
|
3559
|
+
|
|
3560
|
+
The plaintext of a `msg` event is one `MsgBody`, at most `LIMITS.convPlaintextBytes` (64 KiB) of JCS
|
|
3561
|
+
(`too_large`):
|
|
3562
|
+
|
|
3563
|
+
- `line`: `text` (at most `LIMITS.lineChars` (40,000) UTF-16 code units), `format` (`plain` or `md`), and
|
|
3564
|
+
optionally `thread` (the root's event id), `broadcast`, `mentions` (at most `LIMITS.mentions` (50)), `files`
|
|
3565
|
+
(at most `LIMITS.files` (10) attachments), `parts` (up to 8 A2A data parts), `answers` (the mention it answers)
|
|
3566
|
+
`grants` (at most `LIMITS.agentMentions` (5) delegation grants, §6.7) and `via` (§16.10).
|
|
3567
|
+
- `edit {target, text, mentions?}`, `delete {target}`, `react {target, key, on}` and `pin {target, on}`.
|
|
3568
|
+
- `task {task}`: a task as a thread. `request {to, message, delegation}` is the root, to an agent participant;
|
|
3569
|
+
`status` and `artifact` carry the A2A update events (§6.2) and `cancel` and `followUp` the requester's, each
|
|
3570
|
+
naming the root.
|
|
3571
|
+
- `receipt {target, stage}`: `delivered`, `handed_to_agent` or `read`.
|
|
3572
|
+
- `sync {sync}`: a body of the person's `self` conversation, and only there.
|
|
3573
|
+
|
|
3574
|
+
A conversation's name, topic and purpose, and its policy, are not message bodies: they are state, so they are ops
|
|
3575
|
+
(`meta`, `policy`), which never expire.
|
|
3576
|
+
|
|
3577
|
+
A mention is `{p}` (a participant), `{all: "channel"}` or `{all: "here"}`. An attachment names a blob, its own
|
|
3578
|
+
AES-256-GCM key and nonce, its SHA-256 and size (at most `LIMITS.blobBytes` (25 MiB)), a media type and a name.
|
|
3579
|
+
|
|
3580
|
+
**The self conversation.** A person's `self` conversation has one participant, the person, and history `shared`.
|
|
3581
|
+
It holds two kinds of state:
|
|
3582
|
+
|
|
3583
|
+
- **Passing state**, in `sync` bodies (`SyncBody`), which expire with the retention: `read {cid, upTo, thread?}`,
|
|
3584
|
+
`unread {cid, from}`, `follow`, `save`, `draft`, `joined` and `keys {cid, shares}` (at most `LIMITS.keyShares`
|
|
3585
|
+
(512) chain keys).
|
|
3586
|
+
- **Settings**, in `setting` ops (`SettingBody`, sealed), which never expire: `prefs {cid, notify, muteUntil?,
|
|
3587
|
+
receipts?}`, `grant` (a consent grant, §6.9, verified over its raw value), `context {rules}` (§16.8),
|
|
3588
|
+
`localRetention {cid, days}` and `link {link}` (§16.10). The op's `kind` is the body's `s`.
|
|
3589
|
+
|
|
3590
|
+
The last writer wins by `seq`.
|
|
3591
|
+
|
|
3592
|
+
### 16.4 The hash chain and gossip
|
|
3593
|
+
|
|
3594
|
+
- `h(0) = SHA-256("messaging/1/conv-genesis\n" || JCS(cid))`.
|
|
3595
|
+
- `h(seq) = SHA-256(h(seq − 1) || SHA-256(JCS(event)))`, over the raw 32-byte digests.
|
|
3596
|
+
- A member checks `prev = h(seq − 1)` and the signature over the raw received event before it reads any field.
|
|
3597
|
+
- **Gossip.** A member compares an event's `seen` with its own log (`ConvLog.relate`): `ahead` past its head (it
|
|
3598
|
+
fetches up to `seen.seq` first), `consistent` when its `h(seen.seq)` is `seen.hash`, `diverged` otherwise. A
|
|
3599
|
+
divergence proves that the relay showed two members different logs; the signed event is the evidence. `seen`
|
|
3600
|
+
at or past the event's own `seq` is `invalid_request`.
|
|
3601
|
+
- **Device cuts.** A `rekey` with cause `device` ends the named device's authority in this conversation: later
|
|
3602
|
+
entries by that device are invalid. If `until` is present, known entries by that device after `until` and before
|
|
3603
|
+
the rekey are cut from state. An absent `until` makes no earlier cut. The position need not name that device's
|
|
3604
|
+
entry: an expired message is a digest with no author or state effect. Replay collects the cuts, verifies every
|
|
3605
|
+
chain link, and does not apply cut entries; a cut rekey cannot cut another device. A cut that removes genesis or
|
|
3606
|
+
leaves later entries unauthorized is refused. A live verifier checks that the named device has been removed in
|
|
3607
|
+
its verified realm. A cut never extends a removed device's authority.
|
|
3608
|
+
- **Retention.** Only `msg` entries expire. When the relay prunes a `msg` entry older than the conversation's
|
|
3609
|
+
`retainDays` it keeps the entry's `seq`, `prev` and the digest `SHA-256(JCS(event))` and serves those
|
|
3610
|
+
(`ConvExpiredEntry`), so the chain stays verifiable (`ConvLog.applyExpired`). **Ops never expire**: membership,
|
|
3611
|
+
roles, epochs, settings, policy votes, sealed meta and the `self` conversation's settings are ops, so a member
|
|
3612
|
+
that replays the log after pruning, or a joiner reading it from genesis, computes the same state as a member
|
|
3613
|
+
that kept every body. The create (seq 1) never expires (`invalid_request`), and an op head that names an
|
|
3614
|
+
expired entry is `conv_conflict`, so a relay that withholds an op as expired is caught by the next op. A key
|
|
3615
|
+
share (§16.6) is not an entry; the relay deletes it after the retention.
|
|
3616
|
+
|
|
3617
|
+
### 16.5 Sender keys
|
|
3618
|
+
|
|
3619
|
+
- **Chains.** A sender device keeps one chain per (conversation, epoch). From a chain key `ck_i`:
|
|
3620
|
+
- the message key `mk_i = HKDF-SHA256(ikm = ck_i, salt = empty, info = "messaging/1/conv-msg", 32 bytes)`;
|
|
3621
|
+
- the next chain key `ck_{i+1} = HKDF-SHA256(ikm = ck_i, salt = empty, info = "messaging/1/conv-chain", 32
|
|
3622
|
+
bytes)`.
|
|
3623
|
+
|
|
3624
|
+
The first chain of an epoch starts at index 0 from 32 random bytes.
|
|
3625
|
+
- **Sealing a body.** `body = base64url(nonce || AES-256-GCM(mk_idx, nonce, JCS(MsgBody), aad = JCS(header)))`,
|
|
3626
|
+
with a fresh 12-byte nonce and the 16-byte tag last; the header is the event without `body` and `sig`. The
|
|
3627
|
+
event's `epoch` and `idx` name the key. A message key seals one body only.
|
|
3628
|
+
- **Sealed meta.** `create.sealedMeta` is `JCS({name?, topic?, purpose?})` sealed the same way under the creator
|
|
3629
|
+
device's epoch-0 key `mk_0`, with the create event's header as AAD. The creator's first message uses index 1.
|
|
3630
|
+
- **Rotation.** A sender starts a new chain in the same epoch after `LIMITS.chainMessages` (500) messages on one
|
|
3631
|
+
chain, or `LIMITS.chainMaxMs` (7 days) after starting it. The new chain starts from fresh random bytes at the
|
|
3632
|
+
next index, so `(epoch, device, idx)` names every key: the share with the greatest index at or below `idx`
|
|
3633
|
+
derives it.
|
|
3634
|
+
- **Receiving.** A receiver derives forward only and uses each message key once. To open messages that arrive out
|
|
3635
|
+
of order it keeps at most `LIMITS.skippedKeys` (1000) derived but unused keys per (epoch, sender device),
|
|
3636
|
+
dropping the lowest indexes first; a message further than that past the chain is `too_large`, and one whose key
|
|
3637
|
+
is used or dropped is `not_found`. It keeps each share's chain key at the share's index, to share history (§16.6).
|
|
3638
|
+
- **Sealed ops.** A `meta` or `setting` op seals its content (`ConvMeta`, or a `SettingBody` of the op's `kind`)
|
|
3639
|
+
the same way, under the author device's key at the op's (`epoch`, `idx`), with `aad = JCS(event without sig, the
|
|
3640
|
+
op without sealed)`, so the AAD binds the op's kind and key index too. The plaintext is at most
|
|
3641
|
+
`LIMITS.convPlaintextBytes`. A sealed op takes one index of the author's chain, as a message does.
|
|
3642
|
+
- **Epochs.** A `rekey` op starts the next epoch. Each sender then starts a new chain and shares it only with the
|
|
3643
|
+
devices of the participant persons at the rekey's `seq`.
|
|
3644
|
+
- Authenticity comes from the event signature, not from the key: a member who holds a sender key still cannot
|
|
3645
|
+
write as another member. Within an epoch there is no post-compromise security: a copy of a device's state reads
|
|
3646
|
+
every chain it held until the next rotation or epoch.
|
|
3647
|
+
|
|
3648
|
+
### 16.6 Key shares
|
|
3649
|
+
|
|
3650
|
+
A `keys` event goes to **one recipient device**. Its body is `{to: {person, device}, epk, nonce, ct}`; a sender
|
|
3651
|
+
sends one event per recipient device. `ct` seals `JCS({shares: [KeyShare]})`, where a `KeyShare` is `{epoch,
|
|
3652
|
+
device, idx, ck}` (at most `LIMITS.keyShares` (512)):
|
|
3653
|
+
|
|
3654
|
+
1. Generate an ephemeral X25519 key pair; `epk` is its raw public key, `nonce` 12 random bytes.
|
|
3655
|
+
2. `shared = X25519(eph_priv, recipient.enc)`, the recipient device's key as the realm verified it (§4, §15.7).
|
|
3656
|
+
3. `key = HKDF-SHA256(ikm = shared, salt = epk || recipient.enc, info = "messaging/1/seal", 32 bytes)`, as for
|
|
3657
|
+
an envelope (§5, "Sealing").
|
|
3658
|
+
4. `ct = AES-256-GCM(key, nonce, JCS({shares}), aad = JCS({header, to}))`, with the tag last.
|
|
3659
|
+
|
|
3660
|
+
The AAD binds a share to its event and its recipient: moving it to another event or device makes it fail. A
|
|
3661
|
+
receiver keeps the first share for a given (epoch, device, idx); a later one with another key changes nothing.
|
|
3662
|
+
|
|
3663
|
+
**Not an entry.** A `keys` event is not in the hash chain (`ConvLog.apply` refuses one, `invalid_request`). The
|
|
3664
|
+
relay keeps it for its recipient and serves it to no one else (`ConvShareEntry {n, event}`, §7): each device
|
|
3665
|
+
fetches only its own shares, so a rekey costs a device one share per sender, not every share of every sender. A
|
|
3666
|
+
recipient checks a share with `ConvLog.checkShare`, in the state at the share's `opHead`:
|
|
3667
|
+
|
|
3668
|
+
1. I-JSON, size and the schema; the realm and the `cid` (`invalid_request`).
|
|
3669
|
+
2. The signature over the raw event (`bad_signature`).
|
|
3670
|
+
3. Live: `ts` within `LIMITS.clockSkewMs` (`expired`), and `by.device` a current device of `by.person`
|
|
3671
|
+
(`unknown_device`).
|
|
3672
|
+
4. `by.agent` absent (`forbidden`); `opHead` names an op of the log (`conv_conflict`).
|
|
3673
|
+
5. At `opHead`, the author's person is a member participant person (`not_member`) and `to.person` a participant
|
|
3674
|
+
person (`forbidden`); live, `to.device` a current device of `to.person` (`unknown_device`).
|
|
3675
|
+
|
|
3676
|
+
The relay also requires `opHead` to be the current op head (§7), so no share reaches a device whose person was
|
|
3677
|
+
removed since. A relay that withholds a share is not hidden: the recipient cannot open that sender's messages,
|
|
3678
|
+
and shows that it waits for keys.
|
|
3679
|
+
|
|
3680
|
+
### 16.7 Authority
|
|
3681
|
+
|
|
3682
|
+
`ConvLog.apply` checks each entry, in order, against the state after every earlier entry, and a failed check
|
|
3683
|
+
changes nothing:
|
|
3684
|
+
|
|
3685
|
+
1. I-JSON (§2, `invalid_request`) and size (`too_large`); the schema, the realm and the `cid` (`invalid_request`).
|
|
3686
|
+
2. The chain: `seq = head + 1` and `prev = h(head)` (`conv_conflict`).
|
|
3687
|
+
3. The signature over the raw event (`bad_signature`).
|
|
3688
|
+
4. When appending live (the relay, or a member checking a pushed event): `ts` within `LIMITS.clockSkewMs` of now
|
|
3689
|
+
(`expired`), and `by.device` a current device of `by.person` in the realm (`unknown_device`).
|
|
3690
|
+
5. `id` not already in the log (`duplicate`); `seen` before the event (`invalid_request`).
|
|
3691
|
+
6. The first entry is `create`, and there is only one (`conv_conflict`).
|
|
3692
|
+
7. For an `op`: `opHead` is the current op (`conv_conflict`); `by.agent` is absent: an agent never writes an op
|
|
3693
|
+
(`forbidden`). For a `msg`: `opHead` names an op of this log, or `{0, h(0)}` (`conv_conflict`). A `keys` event
|
|
3694
|
+
is never an entry (`invalid_request`, §16.6).
|
|
3695
|
+
8. The rules below. An author who is not a participant is `not_member`, one who lacks the authority `forbidden`, a
|
|
3696
|
+
target that is not a message of this log `not_found`, and a target in the wrong state `invalid_state`.
|
|
3697
|
+
|
|
3698
|
+
A participant **person** is the person of any participant, so the devices of a person whose agent sits in a
|
|
3699
|
+
conversation append its keys and rekeys. "Admin" below means `owner` or `admin`; an author's role is its own
|
|
3700
|
+
participant's.
|
|
3701
|
+
|
|
3702
|
+
| Event | Allowed when |
|
|
3703
|
+
|---|---|
|
|
3704
|
+
| `create` | A **dm**: exactly two seats, both `member`, no visibility, org, permits or meta, history `none`, `cid` the derived one, and the author is the person of one seat. A **self**: one seat, the author as `owner`, `cid` the author's, history `shared`. A **group**: 3 to `LIMITS.groupParticipants` (50) distinct seats, no visibility or org. A **channel**: a visibility, and an org if public, at most `LIMITS.convParticipants` (1000) seats. In a group or channel the author is a seat as `owner`. |
|
|
3705
|
+
| `add` | A group or channel (never a dm or self). The author is a member, not a guest, and an admin when `invite` is `admins`. No seat is a current participant; only an owner adds an owner or an admin; the kind's limit holds (`too_large`). |
|
|
3706
|
+
| `accept` | The author's own participant is `invited`. |
|
|
3707
|
+
| `join` | A public channel the author is not in; live, the author's person is in the channel's org. |
|
|
3708
|
+
| `leave` | A group or channel; the author is a participant. Their agents leave with them. The last owner cannot leave while another person remains. |
|
|
3709
|
+
| `remove` | A group or channel. The author is an admin, and an admin never removes an owner; or the target is the author's own agent (whether or not the author is a participant). Removing a person removes their agents. The last owner cannot be removed. |
|
|
3710
|
+
| `role` | A group or channel. The author is an owner, or an admin setting `member` or `guest` on a participant who is not an owner. The last owner cannot be demoted. |
|
|
3711
|
+
| `seat` | The target is an agent participant whose person is the author. It becomes `member`. |
|
|
3712
|
+
| `settings`, `archive` | A group or channel; the author is an admin. Only a channel has a visibility, and a public one needs an org (`invalid_state`). |
|
|
3713
|
+
| `rekey` | The author's person is a member participant person; `epoch` is the current one plus 1 (`invalid_request`). The cause is due (`invalid_state`), or is `device`, or is `schedule` and the epoch began at least `LIMITS.rekeyScheduleMs` (30 days) before. |
|
|
3714
|
+
| `policy` | Not in a `self` conversation. The author is a member; in a channel, an admin; in a dm or group, any participant person, as a vote. |
|
|
3715
|
+
| `meta` | A group or channel (never a dm or self); the author is a member, and in a channel an admin. Its `epoch` is the current epoch and no rekey is due (`stale_epoch`). |
|
|
3716
|
+
| `setting` | Only in a `self` conversation; the author is its person. Its `epoch` is the current epoch and no rekey is due (`stale_epoch`). |
|
|
3717
|
+
| any `msg` | The author's participant is a member; `epoch` is the current epoch and no rekey is due (`stale_epoch`). |
|
|
3718
|
+
| `line` | Not archived; an admin when `post` is `admins`. A guest may post. `thread` and `answers` name messages of the log. A line with `via` is a person's, never an agent's. |
|
|
3719
|
+
| `edit` | Not archived; the target is a line, not deleted, by the same participant (person and agent). |
|
|
3720
|
+
| `delete` | The target is a line or a task event, not deleted, and the author wrote it or is an admin. |
|
|
3721
|
+
| `react` | As `line`; the target is not deleted; at most `LIMITS.reactionsPerEvent` (50) keys per target (`too_large`). |
|
|
3722
|
+
| `pin` | Not archived; an admin when `pins` is `admins`; the target is not deleted; at most `LIMITS.pins` (100) pinned (`too_large`). |
|
|
3723
|
+
| `task` | A `request` as `line`, to a member agent participant (`not_found`). `status` and `artifact` from that agent; `cancel` and `followUp` from the requester. The root is a task request (`invalid_state`). |
|
|
3724
|
+
| `receipt` | The target is a message of the log. |
|
|
3725
|
+
| `sync` | Only in a `self` conversation, where no other body goes (`invalid_request`). |
|
|
3726
|
+
| `keys` | Not an entry: §16.6 (`ConvLog.checkShare`). |
|
|
3727
|
+
|
|
3728
|
+
**State.** A participant starts as `member`, except an agent whose person is not the author (`invited` until its
|
|
3729
|
+
person seats it) and a `guest` (`invited` until they accept). Live, adding a person of another org as anything but a
|
|
3730
|
+
guest is `forbidden`. A rekey is **due** after a removal or a leave (cause `removed`), an add to a group (`added`),
|
|
3731
|
+
and a change of the history policy (`settings`); a channel does not rekey on add. While a rekey is due, every `msg`
|
|
3732
|
+
is `stale_epoch`: no message is sealed under a key a removed device holds. A `rekey` clears every due cause.
|
|
3733
|
+
|
|
3734
|
+
`ConvLog` keeps the state after every entry: `at(seq)` is the state of a log holding only entries 1 to `seq`. Only
|
|
3735
|
+
ops change it, so the relay, which cannot open bodies, and a member whose message bodies expired compute the same
|
|
3736
|
+
state. The rules that read a message body (edit, delete, reactions, pins, tasks) run only when the caller passes
|
|
3737
|
+
the opened body.
|
|
3738
|
+
|
|
3739
|
+
### 16.8 Agents and history as context
|
|
3740
|
+
|
|
3741
|
+
A person decides which of their agents may mine which of their conversations as context, and a conversation's
|
|
3742
|
+
policy decides whether members' agents may mine it at all.
|
|
3743
|
+
|
|
3744
|
+
- **Policy** (`AgentContext`): `members` (members' agents may, each under its person's rules), `seats` (only
|
|
3745
|
+
agents seated in it, by their seats) or `none`. The default is `members`. In a channel an admin sets it with a
|
|
3746
|
+
`policy` body; in a dm or group each participant person votes, and the strictest vote counts (`none` <
|
|
3747
|
+
`seats` < `members`). A person's vote goes when they leave. The policy is a `policy` op, in plaintext, so a joiner
|
|
3748
|
+
that holds no earlier keys knows it too.
|
|
3749
|
+
- **Rules** (`ContextRule`): `{agent, scope, access, lookbackDays?}`, where `agent` is a session name or `*`,
|
|
3750
|
+
`scope` is `{cid}` or `{kind}` with `kind` one of `own`, `dm`, `group`, `channel.private` and
|
|
3751
|
+
`channel.public`, and `access` is `allow`, `ask` or `off`. A person holds at most `LIMITS.contextRules` (500),
|
|
3752
|
+
one per (agent, scope), synced in their `self` conversation. A conversation is `own` when its participant persons
|
|
3753
|
+
are the person alone.
|
|
3754
|
+
- **Resolution** (`resolveContext`) for agent A of person P reading conversation C as context:
|
|
3755
|
+
1. a `self` conversation is never mined;
|
|
3756
|
+
2. C's policy `seats` or `none` forbids the read, whatever P's rules say;
|
|
3757
|
+
3. else the most specific rule of P applies (a `cid` scope over a `kind` scope, then A by name over `*`), or
|
|
3758
|
+
with none, the default for C's scope:
|
|
3759
|
+
|
|
3760
|
+
| Scope | Access | Lookback |
|
|
3761
|
+
|---|---|---|
|
|
3762
|
+
| `own` | `allow` | the retention |
|
|
3763
|
+
| `channel.public` | `allow` | 90 days |
|
|
3764
|
+
| `channel.private` | `ask` | 30 days |
|
|
3765
|
+
| `group` | `ask` | 30 days |
|
|
3766
|
+
| `dm` | `ask` | 14 days |
|
|
3767
|
+
|
|
3768
|
+
`off` forbids, `ask` holds the read for P to answer, `allow` serves it;
|
|
3769
|
+
4. the lookback is the smallest of the rule's (or default's) lookback, C's `retainDays` and P's local
|
|
3770
|
+
retention for C.
|
|
3771
|
+
- An agent seated in C reads C from its seat by its seat, not by P's rules, unless C's policy is `none`; then it
|
|
3772
|
+
reads only what is addressed to it and the threads that is in (`seatReads`).
|
|
3773
|
+
- Agents of every client follow these rules, MCP clients included; each daemon enforces them for its own person's
|
|
3774
|
+
agents.
|
|
3775
|
+
|
|
3776
|
+
### 16.9 Limits
|
|
3777
|
+
|
|
3778
|
+
| Limit | Value |
|
|
3779
|
+
|---|---|
|
|
3780
|
+
| `LIMITS.convParticipants` | 1000 participants (persons and agents) in a channel |
|
|
3781
|
+
| `LIMITS.groupParticipants` | 50 in a group |
|
|
3782
|
+
| `LIMITS.convOpParticipants` | 100 per `add` or `remove` |
|
|
3783
|
+
| `LIMITS.convEventBytes` | 256 KiB per event |
|
|
3784
|
+
| `LIMITS.convPlaintextBytes` | 64 KiB per message body |
|
|
3785
|
+
| `LIMITS.lineChars` | 40,000 UTF-16 code units |
|
|
3786
|
+
| `LIMITS.mentions`, `LIMITS.files`, `LIMITS.agentMentions` | 50, 10 and 5 per line |
|
|
3787
|
+
| `LIMITS.blobBytes` | 25 MiB per file |
|
|
3788
|
+
| `LIMITS.reactionsPerEvent`, `LIMITS.pins` | 50 keys per target, 100 pins per conversation |
|
|
3789
|
+
| `LIMITS.retainDaysDefault`, `LIMITS.retainDaysMax` | 90 days, 365 days |
|
|
3790
|
+
| `LIMITS.skippedKeys`, `LIMITS.chainMessages`, `LIMITS.chainMaxMs` | 1000, 500, 7 days |
|
|
3791
|
+
| `LIMITS.rekeyScheduleMs` | 30 days |
|
|
3792
|
+
| `LIMITS.keyShares` | 512 chain keys per share |
|
|
3793
|
+
| `LIMITS.wakeHints`, `LIMITS.contextRules` | 5, 500 |
|
|
3794
|
+
|
|
3795
|
+
### 16.10 Linked channels (reserved)
|
|
3796
|
+
|
|
3797
|
+
A person may link an outside chat account (Telegram, then Slack) as an alias of themselves, to reach their own
|
|
3798
|
+
agents from a phone. The fields below are part of the schema and are validated, but no daemon acts on them yet;
|
|
3799
|
+
`docs/design/conversations.md` §17 describes the plan.
|
|
3800
|
+
|
|
3801
|
+
- **`via {channel}`** on a `line`: the person wrote it from a linked channel, `telegram` or `slack`. It is sealed and
|
|
3802
|
+
signed with the event, so every member sees it. Only a person writes it (`forbidden` on an agent's line, §16.7).
|
|
3803
|
+
The person's own devices also check it against the person's links (`checkVia`): a link of that channel, hosted by
|
|
3804
|
+
the authoring device, in force. Other members cannot see the links and do not check that.
|
|
3805
|
+
- **`link`**, a setting of the person's `self` conversation (a sealed `setting` op of kind `link`, which never
|
|
3806
|
+
expires): `{id, channel, account, host, proof, authority, linkedAt, revokedAt?}`.
|
|
3807
|
+
- `account` is the channel's id for the person's account; it is never in the device log, the directory or
|
|
3808
|
+
presence.
|
|
3809
|
+
- `host` is the device that runs the bridge.
|
|
3810
|
+
- `proof` is `{method: "code-sas", codeDigest, sas}`: the digest of the one-time code and the six-digit code the
|
|
3811
|
+
person matched on both sides.
|
|
3812
|
+
- `authority` is a non-empty subset of `chat` and `view` (§8.11), and never more.
|
|
3813
|
+
- For each `id` the record written last is the one in force, unless it has `revokedAt` (`currentLinks`).
|
|
3814
|
+
- **`bridges`** in the `policy` op: who may mirror the conversation to a linked channel: `none` (the default), `admins` or
|
|
3815
|
+
`members`. In a channel an admin sets it. In a dm or group the strictest vote of the participant persons counts,
|
|
3816
|
+
and a person who has not voted counts as `none`, so bridging needs every participant person's vote.
|
|
3817
|
+
- **Reader ids** (`ReaderId`): a person, or `ext:<channel>:<link id>` for a link's channel provider. They are not on
|
|
3818
|
+
the wire; the audience guard's reader sets use them.
|
|
3819
|
+
- A foreign sender on a bridged channel (`via.sender`) is not part of v1: the schema refuses it.
|
|
3820
|
+
|
|
3821
|
+
### 16.11 Same-realm contact request cut-over
|
|
3822
|
+
|
|
3823
|
+
This profile replaces the first-message import rules of §6.9 for conversation DMs. It does not introduce cross-realm addresses.
|
|
3824
|
+
The normative shapes are in `schema/contact-request.ts`. The request plaintext uses `RequestImport`, separate from the ordinary `MsgBody` parser.
|
|
3825
|
+
The cut-over engine uses `openRequestImport` and `applyRequestEntry` for this profile. Source attribution comes from verified request evidence, never the wrapper author.
|
|
3826
|
+
|
|
3827
|
+
**Portable source proof.** A new request contains a sender-signed descriptor and a sealed opening:
|
|
3828
|
+
|
|
3829
|
+
- The descriptor binds `v`, `realm`, `cid`, `from`, `to`, `id`, `issuedAt`, `expiresAt`, `commitment` and `by`.
|
|
3830
|
+
- The sender signs its raw JCS bytes without `sig`, under `DOMAIN.request` (`messaging/1/contact-request\n`).
|
|
3831
|
+
- The opening contains a fresh random 256-bit `salt` and 1–500 UTF-16 code units of `text`.
|
|
3832
|
+
- The commitment is SHA-256 of `DOMAIN.requestContent` and JCS of the opening.
|
|
3833
|
+
- The descriptor digest is SHA-256 of `DOMAIN.requestDigest` and JCS of the signed descriptor.
|
|
3834
|
+
- Both device signatures require bindings from retained, verified identity history. An absent binding fails closed.
|
|
3835
|
+
|
|
3836
|
+
The request names the deterministic DM of two distinct persons. The consent window cannot exceed `LIMITS.ttlMs`.
|
|
3837
|
+
Every device copy carries the same descriptor and opening. Only sealed envelopes carry the opening before acceptance.
|
|
3838
|
+
Requests expose no agent address, task, mention, attachment, context-index entry or wake hint.
|
|
3839
|
+
The envelope payload has `conv: {kind: "request", proof: PortableRequest}`. Its one text part and message ID match the signed opening and request ID.
|
|
3840
|
+
The envelope has human ends, class `request`, and the descriptor's expiry. The cut-over handler rejects a payload without `proof` and asks for resend.
|
|
3841
|
+
The optional field in the stored payload schema permits reading retained old records. It does not authorize old plaintext for a conversation import.
|
|
3842
|
+
|
|
3843
|
+
**Terminal decision.** The recipient signs `RequestDecision` under `DOMAIN.requestDecision` (`messaging/1/contact-request-decision\n`).
|
|
3844
|
+
The decision binds the descriptor digest, request ID, realm, DM, both persons, answer and decision time.
|
|
3845
|
+
The answer is `accept` or `decline`. A local block sends `decline` and keeps the blocking grant private.
|
|
3846
|
+
The decision time must be within the original consent window. Retries preserve the signed decision and its timestamp.
|
|
3847
|
+
|
|
3848
|
+
`request.settle {req, descriptor, decision}` stores the first valid terminal decision in the recipient mailbox.
|
|
3849
|
+
Only the connected recipient device can submit its own signed decision. A later conflicting decision returns the first decision.
|
|
3850
|
+
`request.state {req, descriptor}` returns that decision to either named person, or no decision before settlement.
|
|
3851
|
+
Both frames answer `request.settled {req, id, decision?}`. They expose neither salt nor text to the relay.
|
|
3852
|
+
Relay schema 3 adds `conv_request_decisions`. Generation stays 2 and the minimum reader stays 1.
|
|
3853
|
+
|
|
3854
|
+
**Permanent activation.** An external human DM declares `create.admission: "request"`.
|
|
3855
|
+
The relay refuses an external DM without this declaration. Same-org DMs retain implicit org admission.
|
|
3856
|
+
Before activation, content and key shares fail with `request_required`. The recipient appends `activate {proof: {descriptor, decision}}` after mailbox reconciliation.
|
|
3857
|
+
Device-removal rekeys can precede activation. They still require the normal removed-device and log-authority checks, and grant no content access.
|
|
3858
|
+
The activation is a permanent op. It reserves the request ID for exactly one import.
|
|
3859
|
+
|
|
3860
|
+
The recipient seals `RequestImport {t: "request.import", descriptor, decision, opening}` under its conversation sender key.
|
|
3861
|
+
The wrapper has `id` and `request` equal to the reserved request ID. It has no agent author or wake hint.
|
|
3862
|
+
Members verify both proofs and the opening before they attribute text to the original sender.
|
|
3863
|
+
The recipient then appends `request.complete {request, seq, digest}`. The digest is SHA-256 of JCS of the exact imported event.
|
|
3864
|
+
This permanent completion survives deletion and retention. Ordinary messages wait until all accepted reservations have completed imports.
|
|
3865
|
+
|
|
3866
|
+
Duplicate importers fetch the winning event and verify its exact proofs and opening. A different body at the same ID is a conflict.
|
|
3867
|
+
The source sender need not be online. Crossed requests use one DM but keep separate IDs, answers and imports.
|
|
3868
|
+
The import is immutable. Only its original sender can delete its text. The wrapper author gains no source-author permissions.
|
|
3869
|
+
After retention, members verify the permanent activation and completion without reconstructing text.
|
|
3870
|
+
|
|
3871
|
+
**Existing contacts.** Each person signs a conversation-bound `ContactAuthorization` under `DOMAIN.contactAuthorization`.
|
|
3872
|
+
An activation with `proof.contacts` requires both reciprocal signatures. The daemon signs only under an existing open or muted chat grant.
|
|
3873
|
+
This path needs no new human prompt and imports no request text. A contact card cannot substitute for either signature.
|
|
3874
|
+
|
|
3875
|
+
**Durable local lifecycle.** `ConversationRequests` stores each request and its state in namespaced rows of the existing `kv` table.
|
|
3876
|
+
Queued sends, submitted requests, decisions and imports survive restart. `waitingOnThem()` exposes only explicit submitted requests, not missing receipts.
|
|
3877
|
+
Acceptance opens only chat. A later block prevents local import and key use even after permanent activation.
|
|
3878
|
+
The sender receives the same error for an unknown or undiscoverable exact email.
|
|
3879
|
+
|
|
3880
|
+
Old pending requests retain their local records and expose `resend`. The sender must send a new request with portable evidence.
|
|
3881
|
+
The cut-over never signs old plaintext for its sender or silently resends it.
|