@joe-broadhead/messaging 0.7.1 → 0.9.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 +364 -2
- package/dist/bin/messaging-mcp.js +5498 -4338
- package/dist/bin/messaging-relay.js +8626 -5540
- package/dist/bin/messaging.js +4492 -3158
- package/dist/bin/messagingd.js +19672 -12412
- package/dist/lib/modules/core/agent/operations.js +19 -327
- package/dist/lib/modules/core/agent/present.js +20 -55
- package/dist/lib/modules/core/agent-tools.js +35 -139
- package/dist/lib/modules/core/consent.js +54 -14
- package/dist/lib/modules/core/contact-card.js +3 -3
- package/dist/lib/modules/core/conversation/event.js +29 -4
- package/dist/lib/modules/core/conversation/guest-invitation.js +42 -0
- package/dist/lib/modules/core/conversation/log.js +287 -65
- 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 +90 -37
- 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 +9 -4
- package/dist/lib/modules/core/jelly-host.js +8 -5
- package/dist/lib/modules/core/schema/common.js +33 -5
- package/dist/lib/modules/core/schema/consent.js +23 -5
- package/dist/lib/modules/core/schema/contact-request.js +22 -0
- package/dist/lib/modules/core/schema/conv-frames.js +101 -0
- package/dist/lib/modules/core/schema/conversation.js +67 -33
- package/dist/lib/modules/core/schema/directory-frames.js +8 -3
- package/dist/lib/modules/core/schema/envelope.js +8 -24
- package/dist/lib/modules/core/schema/errors.js +7 -5
- package/dist/lib/modules/core/schema/guest-invitation.js +13 -0
- package/dist/lib/modules/core/schema/index.js +5 -0
- package/dist/lib/modules/core/schema/ipc/consent.js +97 -18
- package/dist/lib/modules/core/schema/ipc/conversation-views.js +87 -0
- package/dist/lib/modules/core/schema/ipc/conversations.js +132 -0
- package/dist/lib/modules/core/schema/ipc/device.js +9 -0
- package/dist/lib/modules/core/schema/ipc/jelly.js +17 -45
- package/dist/lib/modules/core/schema/ipc/shapes.js +74 -73
- 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 +68 -46
- package/dist/lib/modules/core/testing.js +1 -1
- 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 +854 -445
- 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 +295 -442
- package/dist/types/core/consent.d.ts +30 -7
- package/dist/types/core/contact-card.d.ts +3 -3
- package/dist/types/core/conversation/event.d.ts +6 -6
- package/dist/types/core/conversation/guest-invitation.d.ts +8 -0
- package/dist/types/core/conversation/log.d.ts +28 -13
- 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 +32 -9
- package/dist/types/core/conversation-agent-tools.d.ts +939 -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 +11 -6
- package/dist/types/core/jelly-host.d.ts +1 -1
- package/dist/types/core/schema/common.d.ts +33 -6
- package/dist/types/core/schema/consent.d.ts +29 -8
- package/dist/types/core/schema/contact-request.d.ts +264 -0
- package/dist/types/core/schema/conv-frames.d.ts +1204 -0
- package/dist/types/core/schema/conversation.d.ts +1410 -264
- package/dist/types/core/schema/directory-frames.d.ts +8 -3
- package/dist/types/core/schema/envelope.d.ts +85 -130
- package/dist/types/core/schema/errors.d.ts +5 -1
- package/dist/types/core/schema/guest-invitation.d.ts +51 -0
- package/dist/types/core/schema/index.d.ts +5 -0
- package/dist/types/core/schema/ipc/consent.d.ts +464 -62
- package/dist/types/core/schema/ipc/conversation-views.d.ts +1199 -0
- package/dist/types/core/schema/ipc/conversations.d.ts +4355 -0
- package/dist/types/core/schema/ipc/device.d.ts +10 -0
- package/dist/types/core/schema/ipc/jelly.d.ts +16 -111
- package/dist/types/core/schema/ipc/shapes.d.ts +211 -95
- package/dist/types/core/schema/ipc/surfaces.d.ts +26 -427
- package/dist/types/core/schema/ipc/tasks.d.ts +210 -0
- package/dist/types/core/schema/ipc.d.ts +3859 -890
- package/dist/types/core/schema/relay.d.ts +12 -4
- package/dist/types/core/testing.d.ts +1 -1
- 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 +16 -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.
|
|
@@ -56,7 +119,10 @@ Not in v1: hole-punched peer-to-peer transport, federation between relays, and p
|
|
|
56
119
|
`messaging/1/person\n`, `messaging/1/dir-read\n`, `messaging/1/device-log\n`, `messaging/1/device-keys\n`,
|
|
57
120
|
`messaging/1/device-link\n`, `messaging/1/epoch\n` and `messaging/1/dir-key\n`, and for conversations (§16) the
|
|
58
121
|
event signature prefix `messaging/1/conv-event\n` and the hash prefixes `messaging/1/conv-genesis\n`,
|
|
59
|
-
`messaging/1/dm\n` and `messaging/1/self\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.
|
|
60
126
|
- **Hash:** SHA-256, base64url.
|
|
61
127
|
|
|
62
128
|
## 3. Identity
|
|
@@ -419,6 +485,14 @@ An envelope with an unknown `type`, or a newer `v`, is acknowledged to the relay
|
|
|
419
485
|
update without `replyTo` fails the shape check of §5 (`invalid_request`). The requester's daemon
|
|
420
486
|
drops an update whose `replyTo` is not its record of the request (`invalid_request`).
|
|
421
487
|
- The requester's daemon applies them to its `TaskRecord` and publishes a `task` event.
|
|
488
|
+
- **Sign-in.** An executor whose task waits on a sign-in to a tool says which one with `task.update {state:
|
|
489
|
+
TASK_STATE_AUTH_REQUIRED, signIn: {tool}}` (`signIn` with any other state is invalid params). Its daemon puts the
|
|
490
|
+
tool in the status message as A2A v1 extension data: `metadata["urn:messaging:ext:sign-in:v1"] = {tool}`, with that
|
|
491
|
+
URI in the message's `extensions`. The message's parts are the session's `message`, or the fixed text "Sign-in
|
|
492
|
+
required." when it gave none, so the tool name is never copied into text. Extension metadata is how A2A v1 adds
|
|
493
|
+
fields to a `Message` without changing its parts, and a peer that does not know the extension still reads a valid
|
|
494
|
+
status. The requester's daemon stores the status as received and reads the tool only when it projects the task
|
|
495
|
+
(§8.2, "The task view"); nothing else in the extension is read, and whose sign-in it is never comes from the peer.
|
|
422
496
|
- **What the relay sees.** It can link a task request to its updates, and count and time them. It
|
|
423
497
|
learns no content and no state: the state is sealed. This is the correlation a relay needs to
|
|
424
498
|
match a wake ticket to awaited answers.
|
|
@@ -475,7 +549,9 @@ Only the executor changes state (see `canTransition` in core).
|
|
|
475
549
|
still run, and the person is away from that device. The host says so with `person.attached`; a Jelly host's device is
|
|
476
550
|
away until its first `person.attached {attached: true}`, and no other device ever says `away`. A receiver shows that
|
|
477
551
|
device as away (`people.get`), and an `away` older than the online window (§15.9: 150 s) means nothing.
|
|
478
|
-
- `features` advertises optional capabilities. v1 defines `"tasks"
|
|
552
|
+
- `features` advertises optional capabilities. v1 defines `"tasks"`, `"conversations"` (the daemon reads conversation
|
|
553
|
+
logs; `conv.caps` reads it at the relay) and `"reach"` (the daemon enforces the person's reach choices, §6.9; the
|
|
554
|
+
person's own devices show it in `device.list`, §8.2). A receiver ignores a feature it does not know.
|
|
479
555
|
- `label` must equal the sender's roster label. Receivers ignore a presence whose label differs, and
|
|
480
556
|
always show the roster's label.
|
|
481
557
|
- Like every envelope, presence carries the sender's `anchor` in its header (§5). Presence is
|
|
@@ -541,7 +617,7 @@ forwards a credential it was handed.
|
|
|
541
617
|
- `exp`, the earliest along the chain.
|
|
542
618
|
|
|
543
619
|
The requester's record holds the chain it sent.
|
|
544
|
-
- **Delegating (§8.2).** A session delegates with `
|
|
620
|
+
- **Delegating (§8.2).** A session delegates with `task.create {onBehalfOf}`. Only
|
|
545
621
|
the sending daemon signs, and clients never see or build grants.
|
|
546
622
|
- Without `onBehalfOf`, a task starts a new chain whose root is the sending session.
|
|
547
623
|
- A root grant made over IPC is unrestricted: no `scope`, and `maxDepth = LIMITS.delegationDepth`.
|
|
@@ -575,107 +651,26 @@ forwards a credential it was handed.
|
|
|
575
651
|
code: fenced in a model context, `terminalSafe` in a terminal, `cleanPeerText` in a permission
|
|
576
652
|
prompt. A client shows a chain only when it adds something beyond `from`: depth > 1 or a scope.
|
|
577
653
|
|
|
578
|
-
### 6.8
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
always the channel's own name.
|
|
594
|
-
- A DM line: one copy to each machine of the person, and none to the sender's own machines.
|
|
595
|
-
- A DM to oneself (`dm:<own person>`, the person's own conversation, "me"): one copy to each of the
|
|
596
|
-
person's other machines. A line one of the person's agents wrote there is for the person, so every
|
|
597
|
-
machine files it as incoming, unread; a line the person wrote is outgoing. Its lines carry no
|
|
598
|
-
delivery `state`: there is no other person to deliver them to.
|
|
599
|
-
- `create {id, name, persons, creator}`: the full record, one copy to each member machine but the
|
|
600
|
-
sender's. `persons` includes the sender; `creator` is the person who created the channel, so an
|
|
601
|
-
invitee learns it from the inviter's `create`.
|
|
602
|
-
- `invite {id, name, persons}`: the persons added, to the machines of the existing members. Each
|
|
603
|
-
machine of an added person gets a `create` with the full record instead. Persons with no
|
|
604
|
-
machine left on the roster are not in that record.
|
|
605
|
-
- `leave {id, name}`: the sender left, to every member machine.
|
|
606
|
-
- `remove {id, name, persons}`: the persons removed, to every member machine, the removed
|
|
607
|
-
persons' included.
|
|
608
|
-
- `post {id, name}`: a line, to every member machine.
|
|
609
|
-
- The text of a channel op is not filed. Each machine files its own notice line for an op it
|
|
610
|
-
applies, written by the daemon from the op's fields, with the op's `messageId` (§8.2 `notice`).
|
|
611
|
-
- **Receiver rules.** Channels are looked up by `id`, never by name, and an op's `name` must match
|
|
612
|
-
the channel's own name (`not_found`). Where a rule fails, nothing is stored and the sender gets a
|
|
613
|
-
`rejected` receipt.
|
|
614
|
-
- `create` is accepted only for an unknown `id`, only when the sender and this machine's person are
|
|
615
|
-
both in `persons` (`forbidden`), and not for an id this machine
|
|
616
|
-
left or was removed from within the last `LIMITS.ttlMs + LIMITS.clockSkewMs` (`forbidden`). A
|
|
617
|
-
machine holds at most a policy number of channels (the reference daemon: 200); a `create`
|
|
618
|
-
beyond it is `rate_limited`. If this machine already holds a channel with that name, it keeps
|
|
619
|
-
the new one under a local name: the name cut to 28 characters, a hyphen and the lowest free
|
|
620
|
-
number from 2 (`eng-2`), or `rate_limited` when 2 to 99 are all taken. Room references
|
|
621
|
-
(`channel:<name>`), `conv.*`, `search`, `person.send` and `channel.*` use the local name; envelopes always carry the
|
|
622
|
-
channel's own name. This also covers two people creating the same name at once.
|
|
623
|
-
- A `create` or `invite` naming a person this machine cannot admit is `not_found`: in a roster realm a
|
|
624
|
-
person with no current machine; in a directory realm any id that is not a hosted person. A directory
|
|
625
|
-
home takes persons it has not verified yet, then looks them up and watches them (§15.9).
|
|
626
|
-
- `invite` is accepted only from a current member, and only when the channel stays within the
|
|
627
|
-
limit (`forbidden`, `invalid_request`).
|
|
628
|
-
- `leave` removes only the sender. When this machine's person leaves (from another of its
|
|
629
|
-
machines), the channel and its lines are deleted here.
|
|
630
|
-
- `remove` is accepted only from a member who is the channel's creator or a team admin on the
|
|
631
|
-
verified roster (`forbidden`), never naming the sender (`invalid_request`: use `leave`), and only
|
|
632
|
-
for current members (`not_found`). A machine whose person is removed deletes the channel and its
|
|
633
|
-
lines, as on `leave`, and refuses a `create` for that id as above. Other machines remove those
|
|
634
|
-
persons from the channel.
|
|
635
|
-
- `post` is filed only when the sender and this machine's person are both members (`forbidden`).
|
|
636
|
-
- Every name in `persons` must be a person on the roster (`not_found`). All of this is checked
|
|
637
|
-
before anything is stored.
|
|
638
|
-
- A DM line from the receiver's own person is filed in its own conversation, as above.
|
|
639
|
-
- A person who has no machine left on the roster is no longer a member of any channel; each
|
|
640
|
-
machine drops them when it applies the removal.
|
|
641
|
-
- **Delivery.** An accepted chat envelope gets a `delivered` receipt. It creates no inbox item, does
|
|
642
|
-
not count against the inbox quota (§8.3) and never wakes a session. Consent (§6.9) and the inbound
|
|
643
|
-
rate limit still apply. The sender records each chat envelope it routes with its line and
|
|
644
|
-
recipient. A receipt from that recipient's machine, a relay nack, the recipient leaving the roster
|
|
645
|
-
or expiry settles the record, once. An outgoing line is `delivered` once every other person has a
|
|
646
|
-
delivered copy, and `refused` once some person's every copy was rejected or expired; until then
|
|
647
|
-
it is `sent`. When every copy of a channel change to one person is refused, the sender files a
|
|
648
|
-
notice line in that channel naming the person and the code. Notice lines are never sent.
|
|
649
|
-
- **Transcript.** Each daemon keeps one row per line, keyed by `messageId`; a later envelope with a
|
|
650
|
-
`messageId` this machine already holds is not filed and changes nothing. The thread key is the
|
|
651
|
-
first line's `messageId`. Text is the line's text parts joined by newlines, cut to
|
|
652
|
-
`LIMITS.chatChars` UTF-16 code units without splitting a surrogate pair. The sender files its own
|
|
653
|
-
line with the plaintext it sealed; that copy is not on the wire. A person's line that arrives from
|
|
654
|
-
another of their machines is filed as outgoing. Retention deletes lines after
|
|
655
|
-
`retention.transcriptDays` (default 30), and a per-person cap deletes a sender's oldest lines
|
|
656
|
-
first once they hold more than a policy number of lines or bytes (the reference daemon: 10,000
|
|
657
|
-
lines or 16 MiB of UTF-8 text).
|
|
658
|
-
- **Order and read state.** Each daemon numbers its chat changes with one counter that only
|
|
659
|
-
rises and is never reused (`seq`, from 1): every filed line takes the next number, and so does
|
|
660
|
-
every later change of a line (its delivery state) and every channel deleted here. A line keeps the
|
|
661
|
-
number it was filed with as its `seq`, its place in this machine's order; another machine numbers
|
|
662
|
-
the same line differently. Paging and read cursors use it. Read state is per machine and per room:
|
|
663
|
-
a cursor (`readUpTo`, 0 at first) that only moves forward and never past the newest number here.
|
|
664
|
-
A line is unread when another person's line, or the daemon's notice of another person's change,
|
|
665
|
-
was filed after the cursor; this person's own lines never are. Leaving a channel, or being removed
|
|
666
|
-
from it, deletes its cursor with its lines. Nothing syncs read state between machines.
|
|
667
|
-
- **Joining later.** A person invited later does not receive earlier lines. Membership is each
|
|
668
|
-
machine's own view: concurrent invites or removals can leave machines with different member
|
|
669
|
-
lists, and nothing reconciles them. A post refused because of that shows as `refused` to its
|
|
670
|
-
sender.
|
|
671
|
-
- **Reading.** `conv.list`, `conv.read` and `search` (§8.2) see the DMs this machine holds and
|
|
672
|
-
the channels its person is in. Agent-facing clients fence chat text as peer content and ask
|
|
673
|
-
before reading it.
|
|
654
|
+
### 6.8 Conversations: DMs, groups and channels
|
|
655
|
+
|
|
656
|
+
Conversations use the signed log and sender-key encryption in §16. A `cid` identifies a conversation across devices.
|
|
657
|
+
The daemon verifies each prefix before it exposes membership, content or delivery state.
|
|
658
|
+
Legacy chat envelopes and room-name APIs are removed. Existing database tables remain for store compatibility.
|
|
659
|
+
|
|
660
|
+
- **Participants.** A participant is a person or a stable `person/agent` address. Verified bindings identify each signing device.
|
|
661
|
+
- **Admission.** An invitee receives authenticated controls and message digests before acceptance, but no ciphertext or keys.
|
|
662
|
+
Agent-DM admission follows the receiving person's `ask` level (§6.9).
|
|
663
|
+
- **History.** The signed history policy controls access after acceptance. Recovery replaces matching invitation digests with authenticated entries.
|
|
664
|
+
It survives restart and does not restore content deleted by later retention.
|
|
665
|
+
- **Delivery.** Conversation sends return `pending` until verified append evidence arrives. Missing receipts never establish an outgoing consent hold.
|
|
666
|
+
- **Reading.** `conv.list`, `conv.read`, `conv.search` and `conv.context` expose current verified projections.
|
|
667
|
+
Agent reads require a registered session, access checks and the audience guard. Clients fence peer content (§12).
|
|
668
|
+
- **Read state.** Lane 4 omits read positions, unread counts, preferences and follow state. Their methods remain staged until later lanes implement them.
|
|
674
669
|
|
|
675
670
|
### 6.9 Consent
|
|
676
671
|
|
|
677
672
|
Consent belongs to the receiving person. A person's **grants** say what each other person, and the
|
|
678
|
-
|
|
673
|
+
participants of each conversation, may send them and their agents. A device of the person signs each grant,
|
|
679
674
|
only the person's own devices hold it, and the receiving daemon enforces it. Agents never sign or
|
|
680
675
|
change a grant.
|
|
681
676
|
|
|
@@ -694,7 +689,7 @@ A roster realm has only `self` and `org`.
|
|
|
694
689
|
|
|
695
690
|
```ts
|
|
696
691
|
{ v, realm, id: Ulid, person, by: MachineId, ts,
|
|
697
|
-
subject: { person } | {
|
|
692
|
+
subject: { person } | { conv: ConvId } | { relation: "outside" | "org" },
|
|
698
693
|
chat?: "open" | "muted" | "blocked", // lines from the subject (person subjects only)
|
|
699
694
|
agentMail?: "open" | "blocked", // lines from the subject's agents
|
|
700
695
|
ask?: "open" | "request" | "no", // mail and tasks from the subject or their agents to the person's agents
|
|
@@ -706,8 +701,10 @@ A roster realm has only `self` and `org`.
|
|
|
706
701
|
|
|
707
702
|
- `sig` = Ed25519(`by`, `"messaging/1/grant\n"` ‖ JCS(grant without `sig`)). `by` is a device of `person`.
|
|
708
703
|
- `person` and a person subject are `PersonId`s: a roster person name, or a hosted realm's `p_…` id (§15.1).
|
|
709
|
-
- A
|
|
710
|
-
|
|
704
|
+
- A conversation grant sets only `ask` and `agentMail`. Both persons must be active participants in the locally verified conversation.
|
|
705
|
+
- A relation grant records a reach choice ("Reach choices", below): the `outside` grant sets only `chat`, `open` or
|
|
706
|
+
`blocked`; the `org` grant sets only `ask`.
|
|
707
|
+
For an event-specific decision, membership is evaluated at that event's position.
|
|
711
708
|
- `trustedUntil` needs `ask: "open"` and is at most `ts + LIMITS.trustMaxMs` (90 days). Trust is set
|
|
712
709
|
for `LIMITS.trustDefaultMs` (30 days) when the person names no end. An expired `trustedUntil` reads
|
|
713
710
|
as absent.
|
|
@@ -742,8 +739,9 @@ the person:
|
|
|
742
739
|
**Effective policy.** For a sender S, each level is, in order:
|
|
743
740
|
|
|
744
741
|
1. the person grant for S, where it sets the level;
|
|
745
|
-
2. otherwise, for `ask` and `agentMail`, the most permissive
|
|
746
|
-
3. otherwise the
|
|
742
|
+
2. otherwise, for `ask` and `agentMail`, the most permissive grant of a verified conversation shared with S;
|
|
743
|
+
3. otherwise the person's reach choice for S's relation, where they made one ("Reach choices");
|
|
744
|
+
4. otherwise the default for the relation:
|
|
747
745
|
|
|
748
746
|
| Relation | `chat` | `agentMail` | `ask` | trust |
|
|
749
747
|
|---|---|---|---|---|
|
|
@@ -752,8 +750,70 @@ the person:
|
|
|
752
750
|
| `contact` | open | blocked | no | off |
|
|
753
751
|
| `stranger` | `request_required` | blocked | no | off |
|
|
754
752
|
|
|
755
|
-
Only the person grant trusts. A `blocked` chat level also refuses agent mail, asks and
|
|
756
|
-
from S.
|
|
753
|
+
Only the person grant trusts. A `blocked` chat level from the person grant also refuses agent mail, asks and
|
|
754
|
+
channel changes from S.
|
|
755
|
+
|
|
756
|
+
**Reach choices.** A person answers two questions, each Open, Ask me first or Never (owner, 2026-11-10). Ask me
|
|
757
|
+
first is the default, and it is what the relation defaults already do. The person's devices record each answer as
|
|
758
|
+
a grant on a relation subject, so it is signed, merged, re-signed, lapses and syncs like any other grant:
|
|
759
|
+
|
|
760
|
+
| Question | Subject | Open | Ask me first | Never |
|
|
761
|
+
|---|---|---|---|---|
|
|
762
|
+
| "Who can message you" | `{relation: "outside"}` | `chat: "open"` | a grant with no level | `chat: "blocked"` |
|
|
763
|
+
| "How agents may ask yours" | `{relation: "org"}` | `ask: "open"` | `ask: "request"` | `ask: "no"` |
|
|
764
|
+
|
|
765
|
+
No grant for a subject means no choice: the defaults apply, the same as Ask me first. The choices set step 3 of
|
|
766
|
+
"Effective policy", under every person and conversation grant:
|
|
767
|
+
|
|
768
|
+
| S | `chat` under Open | under Ask me first or no choice | under Never |
|
|
769
|
+
|---|---|---|---|
|
|
770
|
+
| `org` | open | open | open |
|
|
771
|
+
| `contact` | open | open | blocked |
|
|
772
|
+
| `stranger` | open | `request_required` | blocked |
|
|
773
|
+
|
|
774
|
+
| S | `ask` under Open | under Ask me first or no choice | under Never |
|
|
775
|
+
|---|---|---|---|
|
|
776
|
+
| `org` | open (`guarded`) | request | no |
|
|
777
|
+
| `contact`, `stranger` | no | no | no |
|
|
778
|
+
|
|
779
|
+
- **"Who can message you"** is about the people outside the person's org. The org stays `open`. A `contact` has
|
|
780
|
+
already been let in (the person accepted their request, sent them one, or granted them something), so Ask me
|
|
781
|
+
first leaves them `open`, as it did before the choice existed; Never blocks one whose person grant sets no `chat`.
|
|
782
|
+
So no answer opens anything the person had closed, and the default changes nothing.
|
|
783
|
+
- **"How agents may ask yours"** is about the agents of the person's org. Agents of people outside the org stay
|
|
784
|
+
`no`: only a person or conversation grant lets them ask.
|
|
785
|
+
- **A grant set for one person or conversation wins.** A person grant's `chat` (an accepted request sets `open`)
|
|
786
|
+
and its `ask`, and a conversation grant's `ask` and `agentMail`, all come before the choice. So a conversation
|
|
787
|
+
grant with `ask: "open"` still lets the agents of that conversation's participants ask under Never: an explicit
|
|
788
|
+
grant for one person or conversation wins over the account-wide choice (coordinator, 2026-11-10, under the owner's
|
|
789
|
+
rule).
|
|
790
|
+
- A `blocked` chat level that comes from Never refuses S's new lines, message requests and channel changes as they
|
|
791
|
+
arrive. For conversations that already exist, Never hides S's lines and channel-change notices from the person's
|
|
792
|
+
view; it can't undo a change S makes to a shared conversation. It does not refuse agent mail or asks: they follow
|
|
793
|
+
their own levels, so a grant that opened them still counts.
|
|
794
|
+
- **In conversations that already exist** the same rule applies on the person's own view: the daemon's reads of a
|
|
795
|
+
conversation (`conv.read`, `conv.sync`, `conv.search`, `conv.context` and the stream) leave out the lines and
|
|
796
|
+
channel-change notices of S while S's effective `chat` is `blocked`, whether a person grant or Never set it. Never
|
|
797
|
+
leaves out only an S the realm's verified profile shows in another org (`org: "other"`); the org is never left out,
|
|
798
|
+
and neither is a person the realm no longer knows (a departed teammate), history included (owner, 2026-11-10). A
|
|
799
|
+
person grant's block still leaves out anyone. An agent of S is left out only by a person grant's block (agent
|
|
800
|
+
mail follows its own level). A channel change S makes is still applied to the conversation's state; only its
|
|
801
|
+
notice is left out of the person's view.
|
|
802
|
+
Like a person-grant block, this filters what the person sees and is undone when the choice changes. The
|
|
803
|
+
conversation's log, and the other participants' views of it, are unchanged.
|
|
804
|
+
- **Signers.** The rule of "Signers" applies as written: `chat: "blocked"` and `ask: "no"` are kept when their
|
|
805
|
+
signer is removed; `chat: "open"`, `ask: "open"` and `ask: "request"` lapse into the review list, and the choice
|
|
806
|
+
goes back to none. A grant with no level lapses whole.
|
|
807
|
+
- **Cards.** A card computes its levels with the choices (`cardCan`). A change of a choice sends no cards: a card
|
|
808
|
+
is advice, and S learns on its next refusal.
|
|
809
|
+
- **Store.** The daemon keeps relation grants and their review entries in tables of their own (§8.8, schema 5),
|
|
810
|
+
not in `grants`: a daemon from before schema 5 drops a row of `grants` that does not verify for it, so a host
|
|
811
|
+
that rolled the binary back would lose the person's choices.
|
|
812
|
+
- **Older daemons do not enforce the choices.** A daemon from before schema 5 keeps the relation grants it is sent
|
|
813
|
+
out of its view, so it applies the relation defaults: a stranger may send it a message request again, and a
|
|
814
|
+
teammate's ask is held there, not refused. Envelopes go to every device of the person, so after a binary rollback,
|
|
815
|
+
or while any of the person's devices runs an older release, a choice that narrows reach (Never) is not enforced on
|
|
816
|
+
that device. A client should warn while any of the person's devices reports a store schema below 5.
|
|
757
817
|
|
|
758
818
|
**Contact cards.** A person's card tells another person what they may send the person. The schema is
|
|
759
819
|
`ContactCard` in `schema/consent.ts`:
|
|
@@ -767,7 +827,7 @@ from S.
|
|
|
767
827
|
- `sig` = Ed25519(`by`, `"messaging/1/card\n"` ‖ JCS(card without `sig`)). `by` is a device of `from`; `from` and
|
|
768
828
|
`to` are different persons. `trustedUntil` needs `ask: "open"`. A card is at most `LIMITS.cardBytes` of JCS.
|
|
769
829
|
- `can` is the person grant for S over the defaults of S's relation to R (core's `cardCan`): `chat` is true for
|
|
770
|
-
`open` and `muted`, `agentMail` for `open`.
|
|
830
|
+
`open` and `muted`, `agentMail` for `open`. Conversation grants are not on a card.
|
|
771
831
|
- `seq` rises with every card R's devices send S; S keeps the card with the highest `seq`.
|
|
772
832
|
- Core's `verifyContactCard` checks the schema, the rules, the size, the realm, both persons and the signature over
|
|
773
833
|
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
|
|
@@ -782,11 +842,11 @@ from S.
|
|
|
782
842
|
| Anything from the receiver's own person | Deliver, `self` |
|
|
783
843
|
| An answer to the person's own traffic: a receipt, `task.status`, `task.artifact`, `task.cancel`, or a message the counterpart binding (§5) ties to earlier traffic | Deliver, whatever the grants, so a grant change never strands a task the person started. `trusted` while the person grant trusts the sender, else `guarded` |
|
|
784
844
|
| Anything else, while `chat` is `blocked` | Refuse, `refused` |
|
|
785
|
-
| A DM line or channel post by the person S | `chat`: `open` delivers, `muted` delivers silently, `request_required` refuses with that code |
|
|
786
|
-
| A line or channel post by an agent of S | `agentMail`: `open` delivers; otherwise refuse, `refused` (`request_required` for a `stranger` when no grant set it) |
|
|
787
|
-
| A channel change | Deliver; a `stranger` is refused with `request_required` |
|
|
788
|
-
| Mail or a task request to one of the person's agents (an ask) | `ask`: `open` delivers `guarded`, or `trusted` (below); `request` holds it for the person; `no` refuses, `request_required` when it is the default of a `contact` or a `stranger
|
|
789
|
-
| A message request (class `request`, "Message requests") |
|
|
845
|
+
| A DM line or channel post by the person S | `chat`: `open` delivers, `muted` delivers silently, `blocked` (from Never) refuses with `refused`, `request_required` refuses with that code |
|
|
846
|
+
| A line or channel post by an agent of S | `agentMail`: `open` delivers; otherwise refuse, `refused` (`request_required` for a `stranger` when no grant set it and the person did not choose Never) |
|
|
847
|
+
| A channel change | Deliver; refuse while `chat` is `blocked` (from Never), `refused`; a `stranger` is refused with `request_required` |
|
|
848
|
+
| Mail or a task request to one of the person's agents (an ask) | `ask`: `open` delivers `guarded`, or `trusted` (below); `request` holds it for the person; `no` refuses, `request_required` when it is the default of a `contact` or a `stranger` and the person did not choose Never for messages, else `refused` |
|
|
849
|
+
| A message request (class `request`, "Message requests") | While `chat` is `request_required` (a `stranger` under Ask me first): hold it for the person. While `chat` is `blocked` (from Never): refuse, `refused`. Otherwise: as a DM line by the person S |
|
|
790
850
|
|
|
791
851
|
- **Trust and the chain rule.** An ask runs `trusted` only while the person grant's `trustedUntil` is
|
|
792
852
|
past now and every person in its delegation chain (§6.7) is S or the receiver. Otherwise it runs
|
|
@@ -824,40 +884,27 @@ from S.
|
|
|
824
884
|
A delivered request then passes the inbound policy of §8.3 as any other;
|
|
825
885
|
- a request ends unanswered at its expiry: the sender's own expiry settles its side. A request
|
|
826
886
|
whose sender has left the realm ends at the answer, with nothing sent.
|
|
827
|
-
- **Message requests** (a directory realm only).
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
agents never send or answer one.
|
|
887
|
+
- **Message requests** (a directory realm only). Human first contact across orgs uses `contact.request` and a portable request proof (§16).
|
|
888
|
+
Its `message` envelope carries `conv: {kind: "request", proof}` and one text part, bounded by `LIMITS.requestChars`.
|
|
889
|
+
The envelope expiry must equal the signed descriptor expiry. Agents cannot send or answer contact requests.
|
|
831
890
|
- **Addressing.** The person gives an exact email. The daemon resolves it at the directory (`dir.resolve
|
|
832
891
|
{email}`, §7); no answer is `not_found`, "no Messaging user with that email accepts requests", the same for an
|
|
833
892
|
unknown and an undiscoverable email, so it cannot tell them apart. Several persons for one email is
|
|
834
893
|
`ambiguous_address`.
|
|
835
|
-
- **Sending.** The
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
from a blocked sender it is `refused`. A held request is stored with its envelope and opened payload (§8.8,
|
|
846
|
-
`contact_requests`), one per sending person and at most `LIMITS.heldTotal` in all (`rate_limited`), answered with a
|
|
847
|
-
`held` receipt and filed as a `request` notice (§8.2). It reaches no agent and wakes nothing. A request in a
|
|
848
|
-
roster realm is `invalid_request`: a roster realm is one org.
|
|
849
|
-
- **Answering** (`request.answer`): `accept` sets the sender's grant to `chat: "open"` (keeping its other
|
|
850
|
-
levels), which makes the sender a `contact` and sends them a card, then files the request's text as their
|
|
851
|
-
first line in `dm:<sender>` and answers `delivered`; `decline` answers `rejected {code: "declined"}`; `block`
|
|
852
|
-
sets `chat: "blocked"`, then declines. A request ends unanswered at its expiry. Every device of the person
|
|
853
|
-
holds its own copy: a grant for the sender that another device of the person signed, arriving by "Sync", settles
|
|
854
|
-
the copy held here as its answer did (`chat: "open"` accepts it, `blocked` declines it).
|
|
894
|
+
- **Sending.** The daemon sends one copy to each current recipient device and returns `{id,cid,person}`.
|
|
895
|
+
It stores the request separately from conversation history. An authenticated recipient receipt establishes an outgoing hold.
|
|
896
|
+
A missing receipt means pending delivery, not a hold. One request can wait per person, subject to `LIMITS.requestsPerDay`.
|
|
897
|
+
- **Receiving.** The daemon verifies the portable proof, envelope binding and expiry before it stores the request.
|
|
898
|
+
A stranger's request waits for the person. An allowed contact or same-org request follows the signed acceptance and import path automatically.
|
|
899
|
+
A blocked sender is refused. The opening reaches no agent and triggers no wake before or after import.
|
|
900
|
+
- **Answering.** `request.answer {answer:"accept"}` settles a signed mailbox decision, grants chat access, and imports exactly one opening into the deterministic DM.
|
|
901
|
+
The import uses the descriptor ID and permanent completion evidence. Concurrent accepting devices reconcile against the same verified operations and completion.
|
|
902
|
+
`decline` settles a signed decline. `block` also sets `chat:"blocked"`. A grant or card alone is not a terminal decision.
|
|
903
|
+
The daemon resumes interrupted acceptance after restart and sends a contact card after successful import.
|
|
855
904
|
- **The sender's card is the directory's.** Clients show the sender of a request by the realm's verified profile
|
|
856
905
|
(name, email, org, §15.1), never by the request's text.
|
|
857
|
-
- **Agents.**
|
|
858
|
-
|
|
859
|
-
`request.list` and filed as a `request` notice; the person sends the request with `contact.request`
|
|
860
|
-
(`suggestion` dismisses it), or dismisses it with `request.answer {answer: "decline"}`.
|
|
906
|
+
- **Agents.** Legacy contact suggestions remain dismissible. The removed `msg.send` method cannot create new suggestions.
|
|
907
|
+
Agent-to-agent DMs use recipient invitation admission instead of human contact requests.
|
|
861
908
|
- **Cards on the daemon.** A device sends a card (a `message` with `conv: {kind: "card", what: "card"}`) to each
|
|
862
909
|
current device of a person S when this person's grant for S changes on this device (`grant.set`, an answer to
|
|
863
910
|
a request, a re-sign under "Signers"), and when S asks with `card.get`. It sends none to a `stranger` and none to
|
|
@@ -868,14 +915,7 @@ from S.
|
|
|
868
915
|
person; it answers nothing. A `rejected` receipt from a person with `refused`, `request_required` or `declined`
|
|
869
916
|
drops their card and asks for a fresh one. The `seq` of a device's cards is its clock in milliseconds, raised
|
|
870
917
|
past the last one it used. A card is not a first message for the freshness bound (§15.9).
|
|
871
|
-
- **Pacing.**
|
|
872
|
-
limited to `LIMITS.a2hPerHour` (30) an hour and `LIMITS.a2hPerDay` (100) a day:
|
|
873
|
-
- the sending daemon refuses an agent's DM line or channel post at send time (`msg.send` to a person
|
|
874
|
-
or `#channel`), before it stores or sends anything, when it would take any recipient person past either
|
|
875
|
-
count, with `rate_limited` and when to try again; the person's own lines are not paced;
|
|
876
|
-
- the receiving daemon refuses an agent's line past either count from one sending person with a
|
|
877
|
-
`rejected` receipt, `rate_limited`, for a sender that does not pace itself;
|
|
878
|
-
- the reference daemon counts in memory, so a restart starts the windows again.
|
|
918
|
+
- **Pacing.** Conversation append limits replace legacy chat-envelope pacing (§16). Retained task envelopes still follow the relay and inbound limits (§7, §8.3).
|
|
879
919
|
- **Sync.** A device sends each grant it signs or re-signs to each of its person's other current
|
|
880
920
|
devices: a sealed `message` to the device with no agent, `conv: {kind: "self", what: "grant"}`,
|
|
881
921
|
whose one data part is `{grants}`. A device of the person that is new to the sender (one it has
|
|
@@ -1210,7 +1250,7 @@ hosted `hello` as a malformed one (4001).
|
|
|
1210
1250
|
| `dir.watch {persons}`, at most `LIMITS.watchPersons` (500) | `dir.ok {}` |
|
|
1211
1251
|
| `presence.watch {persons}`, at most `LIMITS.watchPersons` | `dir.ok {}` |
|
|
1212
1252
|
| `dir.erase {entries, token}`: an `unbind` of each current device, at most `LIMITS.devicesPerPerson` (10), and a compact JWS | `dir.ok {}` |
|
|
1213
|
-
| `wake.arm {url, ticket, agents, awaiting}`: a Jelly host's wake ticket ("Wake tickets" below) | `dir.ok {}` |
|
|
1253
|
+
| `wake.arm {url, ticket, agents, awaiting, dms?}`: a Jelly host's wake ticket ("Wake tickets" below) | `dir.ok {}` |
|
|
1214
1254
|
| `wake.disarm {}` | `dir.ok {}` |
|
|
1215
1255
|
|
|
1216
1256
|
- **`dir.sthIs`.** The STH at `size`, or the latest. `head` is its last epoch-log leaf (index `size − 1`) with
|
|
@@ -1246,7 +1286,7 @@ hosted `hello` as a malformed one (4001).
|
|
|
1246
1286
|
daemon arms it at its relay (`wake.arm`, §8.11). While that device is offline, the relay may present the ticket to
|
|
1247
1287
|
Jelly ID, which wakes the host.
|
|
1248
1288
|
|
|
1249
|
-
- **`wake.arm {url, ticket, agents, awaiting}`**, after `hello` (`forbidden` before it), from a current device:
|
|
1289
|
+
- **`wake.arm {url, ticket, agents, awaiting, dms?}`**, after `hello` (`forbidden` before it), from a current device:
|
|
1250
1290
|
- `url` is Jelly ID's wake endpoint of this device's host, `https://<issuer host>/hosts/<hst>/wake` (`WakeUrl`): its
|
|
1251
1291
|
origin must be the realm's Jelly ID issuer's (`invalid_request`), and `<hst>` the `hst` of the Jelly token of this
|
|
1252
1292
|
device's latest `bind` or `attest` that names one (`forbidden` otherwise, and for a device no host bound);
|
|
@@ -1254,20 +1294,38 @@ Jelly ID, which wakes the host.
|
|
|
1254
1294
|
unverified (`readWakeTicket`), only the header's `typ` and `alg` (`jelly-wake+jwt`, `EdDSA`), and the claims `exp`
|
|
1255
1295
|
(it must be in the future and at most seven days and 60 s ahead), `aud` (when present, exactly `url`) and `sub`
|
|
1256
1296
|
(when present, exactly `<hst>`); any other is `invalid_request`;
|
|
1257
|
-
- `agents` (at most 64 session names) are the device's sessions whose own person's mail may wake the host,
|
|
1258
|
-
`awaiting` (at most 500 envelope ids) the requests whose task updates may wake it
|
|
1297
|
+
- `agents` (at most 64 session names) are the device's sessions whose own person's mail may wake the host,
|
|
1298
|
+
`awaiting` (at most 500 envelope ids) the requests whose task updates may wake it, and `dms` (at most `WAKE_DMS`
|
|
1299
|
+
(256) conversation ids; absent is none) the dm conversations in which another person's events may wake it. The
|
|
1300
|
+
host's daemon chooses them (§8.11, "Wake tickets"); the relay checks only their form and the cap.
|
|
1259
1301
|
|
|
1260
1302
|
It replaces the device's earlier ticket, and is kept in the relay's store (`dir_wakes`, §8.8) until it is
|
|
1261
1303
|
used, disarmed, its device is unbound or erased, or it expires. At most 60 arms a device in a burst, then one a
|
|
1262
1304
|
minute (`rate_limited` with `retryAfterMs`). `wake.disarm {}` forgets the device's ticket.
|
|
1263
|
-
- **When the relay presents it.** Only
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
`
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1305
|
+
- **When the relay presents it.** Only while the device has no connection, and only for one of these:
|
|
1306
|
+
- **a conversation line by the device's own person to an armed agent.** The relay has just stored (`conv.ok`, §7
|
|
1307
|
+
"Conversation frames") a `msg` event whose `wake` hint (§16.2) names a session in `agents`. The person is the one
|
|
1308
|
+
of the authenticated device that appended the event (which `by.person` must match), never the event's claims
|
|
1309
|
+
alone, and only that person's current devices are considered. A person's own mail to their agent is such a line
|
|
1310
|
+
(design §8.2, "Wake"): their daemon sets `wake` to the sessions of their own agents a line addresses, a dm whose
|
|
1311
|
+
other end is one of them or a mention of one, and never names another person's session. A `wake` hint on another
|
|
1312
|
+
person's event reaches only that person's own devices, so it never wakes anyone else's host. A repeated append
|
|
1313
|
+
(`conv.ok` again for the same event) does not present again. The reason logged is `own`;
|
|
1314
|
+
- **an event in an armed dm by its other person** (owner, 2026-11-10, opencode-messaging #19). The relay has just
|
|
1315
|
+
stored a `msg` event in a conversation whose kind is `dm` (its plaintext `create` op) and whose `cid` is in the
|
|
1316
|
+
device's `dms`, and the authenticated author's person is not the device's person. Only the current devices of
|
|
1317
|
+
the conversation's participant persons other than the author's are considered. The relay cannot tell a line
|
|
1318
|
+
from another sealed event (a reaction, an edit, a receipt), so any such `msg` may wake; the budget below bounds
|
|
1319
|
+
it. Ops, the person's own events (their own lines wake only by the hint above) and a dm the device did not arm
|
|
1320
|
+
never wake by this rule. Arming learns the relay nothing new: dm membership is plaintext to it. A repeated
|
|
1321
|
+
append does not present again. The reason logged is `dm`, without the conversation;
|
|
1322
|
+
- **an envelope** the relay has just taken (§7 "Routing": first seen, queued) for the device that is:
|
|
1323
|
+
- the person's own mail to an armed agent: header type `message`, `from.person` = `to.person`, and `to.agent` in
|
|
1324
|
+
`agents` (a task request, which keeps its envelope); or
|
|
1325
|
+
- an answer the device awaits: header type `task.status` or `task.artifact` whose `replyTo` is in `awaiting`.
|
|
1326
|
+
|
|
1327
|
+
Conversation events without a `wake` hint (posts, receipts, another person's lines) outside an armed dm, presence, task cancels, anyone
|
|
1328
|
+
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
|
|
1271
1329
|
must answer (§6.9) is held by the receiving daemon, which the relay cannot see: such mail never wakes unless it is
|
|
1272
1330
|
the person's own.
|
|
1273
1331
|
- **Presenting.** `POST <url>` with the form `client_assertion_type=urn:jelly:wake&ticket=<ticket>`, no redirect
|
|
@@ -1280,8 +1338,8 @@ Jelly ID, which wakes the host.
|
|
|
1280
1338
|
- `503`, any other 5xx, or no answer: kept, and presented again a minute later if the device is still offline. That
|
|
1281
1339
|
retry has waited out the gap on its own timer and is not held back again by the one-a-minute rule, however the
|
|
1282
1340
|
relay's clock and its timers drift; new traffic within the minute is.
|
|
1283
|
-
- **Never logged.** The ticket is in no log line
|
|
1284
|
-
`retry`) and the status.
|
|
1341
|
+
- **Never logged.** The ticket is in no log line, nor are the armed `dms`; a presentation is logged with the
|
|
1342
|
+
device, why (`own`, `dm`, `awaited`, `retry`) and the status.
|
|
1285
1343
|
- **What a relay can and cannot do with a ticket.** A relay that keeps or leaks a ticket can, at most, wake that one
|
|
1286
1344
|
host once, before the ticket's `exp`: Jelly ID spends its `jti` on the wake, checks that it is that host's (its
|
|
1287
1345
|
signature by the host's enrolled key, `iss`, `sub` and `aud`), and holds every host to its budget (6 an hour, 48 a
|
|
@@ -1289,9 +1347,83 @@ Jelly ID, which wakes the host.
|
|
|
1289
1347
|
waking a host gives the presenter nothing on it: the host's daemon then reconnects and fetches its mail as usual.
|
|
1290
1348
|
Nor can a relay mint or widen one: it cannot sign for the host's key, and an `aud` names one host's URL. The relay
|
|
1291
1349
|
learns that the host sleeps (it already sees the device go offline), which agents may wake it (presence already
|
|
1292
|
-
names them),
|
|
1350
|
+
names them), the ids of the task requests it awaits (envelope ids it already routed), and which of its dms may
|
|
1351
|
+
wake it (the relay already holds every dm's membership). A malicious relay can also
|
|
1293
1352
|
fail to wake a host, as it can drop any envelope; that delays the person's mail, it loses none.
|
|
1294
1353
|
|
|
1354
|
+
**Conversation frames** (`schema/conv-frames.ts`; §16). After `hello`, in a roster realm and in a directory realm
|
|
1355
|
+
alike, the relay sequences conversation logs. It is never trusted for authority: it sequences and filters, and
|
|
1356
|
+
members verify every entry (§16.4, §16.7). Every frame is strict; a refusal is `nack {id: req, code, message}`.
|
|
1357
|
+
|
|
1358
|
+
- **`conv.append {req, event}`**, an `op` or a `msg` (a `keys` event is `invalid_request`: shares go in
|
|
1359
|
+
`conv.share`). The relay checks, in order:
|
|
1360
|
+
1. the frame's schema and `LIMITS.convEventBytes` (`too_large`); `by.device` is the authenticated device
|
|
1361
|
+
(`forbidden`);
|
|
1362
|
+
2. the rate limits below (`rate_limited` with `retryAfterMs`);
|
|
1363
|
+
3. a `create` against an empty log first, so a person who could not create it learns nothing of an existing one;
|
|
1364
|
+
then a `create` of an existing conversation is `conv_conflict`;
|
|
1365
|
+
4. a person who is not a participant person of an existing conversation gets `not_found`, as for a conversation
|
|
1366
|
+
that does not exist (no existence oracle for derived dm ids), except for a `join`;
|
|
1367
|
+
5. `ConvLog.apply` against the relay's copy of the log at its head, with the live checks (§16.7, step 4): `ts`
|
|
1368
|
+
within the clock skew, `by.device` a current device of `by.person`, and the org of a person for `join` and
|
|
1369
|
+
`add`. In a directory realm the devices and orgs are the directory's (§15.10); in a roster realm they are the
|
|
1370
|
+
roster's, and the realm is one org, its team id. An `op` whose `opHead` is not the current op head is
|
|
1371
|
+
`conv_conflict`; a `msg` under another epoch, or while a rekey is due, is `stale_epoch`;
|
|
1372
|
+
6. every person a `create` or `add` names has a current device in the realm (`not_found`), and no person would
|
|
1373
|
+
take part in more than `LIMITS.convsPerDevice` (5000) conversations (`too_large`).
|
|
1374
|
+
|
|
1375
|
+
It then assigns `seq`, stores the entry and answers `conv.ok {req, cid, seq, hash}` once it is committed (the
|
|
1376
|
+
group commit, as for `send`), and pushes `conv.push {cid, entry}` to the other online devices of every participant
|
|
1377
|
+
person before and after the entry, so a removed person's devices see their removal. A repeat of an event the log
|
|
1378
|
+
holds (the same JCS) is answered `conv.ok` again, with its first `seq`; another event with a used `id` is
|
|
1379
|
+
`duplicate`.
|
|
1380
|
+
- **`conv.share {req, events}`**: 1 to `LIMITS.convSharesPerFrame` (100) key shares (§16.6), all of one
|
|
1381
|
+
conversation and by the authenticated device. Each must pass `ConvLog.checkShare` live, with `opHead` the current
|
|
1382
|
+
op head (`conv_conflict`), so a person removed since gets none. All or none are stored; the relay answers
|
|
1383
|
+
`conv.shared {req, cid, count}` and pushes `conv.push {cid, share: {n, event}}` to each recipient device online,
|
|
1384
|
+
and to no other device. A previously stored share repeated with identical JCS is acknowledged without storing or
|
|
1385
|
+
pushing it again, even if the op head has since advanced. A used id with different content is `duplicate`. A new
|
|
1386
|
+
share must still pass all current checks; the repeat rule never delivers an old share to a removed device.
|
|
1387
|
+
- **`conv.fetch {req, cid, after, limit?, sharesAfter?}`** → `conv.entries {req, cid, head, entries, shares, more}`:
|
|
1388
|
+
the entries after `after` in `seq` order, at most `limit` (`LIMITS.convFetchEntries` (500)) and as many as fit in
|
|
1389
|
+
three quarters of a frame (at least one). An expired `msg` is served as `{seq, prev, digest}`; an op never is. With
|
|
1390
|
+
`sharesAfter`, also this device's shares after that `n`; no other device's share is ever served. `more` says
|
|
1391
|
+
there is more of either. Only a device of a participant person may fetch (`not_found` otherwise).
|
|
1392
|
+
- **`conv.head {req, cids? | after?}`** → `conv.heads {req, heads: [{cid, head, opHead, shares}], more}`: the heads
|
|
1393
|
+
of the named conversations this device's person takes part in, or, without `cids`, of all of them by `cid` after
|
|
1394
|
+
`after`, at most `LIMITS.convHeads` (500) each way. `shares` is the last share `n` for this device (0: none). A
|
|
1395
|
+
device that comes back online asks this first, then fetches what moved.
|
|
1396
|
+
- **`conv.caps {req, persons}`** → `conv.capabilities {req, outdated, unknown?}`: of at most `LIMITS.convOpParticipants` named
|
|
1397
|
+
persons, those every one of whose current devices with a stored, unexpired presence names no `conversations`
|
|
1398
|
+
feature: a daemon from before conversations, which reads no log. A person with no such presence is not named, since
|
|
1399
|
+
no presence is no evidence. It is a hint from the relay and can only make a sender refuse: before it creates a dm,
|
|
1400
|
+
group or channel with that person, adds them, or sends them a message request, a daemon asks, and refuses with
|
|
1401
|
+
`peer_outdated` ("<handle> needs to update Messaging"), creating and sending nothing. It reveals one bit about a
|
|
1402
|
+
person of the realm, whom the asker can already name. So that it is no presence oracle, a person the asker does not
|
|
1403
|
+
already relate to (the presence audience of §15.10: the same person, the same org, envelopes both ways in 30
|
|
1404
|
+
days, or accepted members of one conversation; a roster realm is one org) counts against a budget of
|
|
1405
|
+
`LIMITS.requestsPerDay` distinct persons per asking person (not per device) per UTC day, the message-request
|
|
1406
|
+
budget. Related persons, and persons already asked about that day, are always answered. A new unrelated person past
|
|
1407
|
+
the budget is not answered: the reply names them in `unknown` (present only when not empty), and the daemon goes
|
|
1408
|
+
ahead for them as without an answer. Every other person in the same frame is still answered, so a daemon refuses
|
|
1409
|
+
with `peer_outdated` for any person the reply names in `outdated`, whatever else the frame holds.
|
|
1410
|
+
- **Retention.** Once a minute the relay prunes the bodies of `msg` entries older than the conversation's
|
|
1411
|
+
`retainDays` (as of its head), by the time it sequenced them, keeping each entry's `seq`, `prev` and digest
|
|
1412
|
+
(§16.4), and deletes the key shares older than it. It never prunes an op.
|
|
1413
|
+
- **Limits** (per conversation and device, in memory; a restart refills them):
|
|
1414
|
+
- appends by one device to one conversation: a burst of `LIMITS.convAppendBurst` (20), refilled at
|
|
1415
|
+
`LIMITS.convAppendsPerMinute` (60) a minute; by all its devices, `LIMITS.convAppendsPerConvPerMinute` (600) a
|
|
1416
|
+
minute;
|
|
1417
|
+
- `rekey` ops whose cause the log cannot prove (`device`, `schedule`): `LIMITS.convRekeysPerHour` (4) per device
|
|
1418
|
+
per conversation an hour;
|
|
1419
|
+
- creates: `LIMITS.convCreatesPerHour` (60) per device an hour;
|
|
1420
|
+
- key shares: `LIMITS.convSharesPerMinute` (10,000) per device per conversation a minute, and at most
|
|
1421
|
+
`LIMITS.convSharesPerPair` (256) kept from one sender device for one recipient device in one conversation
|
|
1422
|
+
(`rate_limited`), so one member cannot fill another's shares.
|
|
1423
|
+
- **What the relay learns.** The plaintext ops (participants, roles, settings, epochs, policies), the kind of each
|
|
1424
|
+
sealed op (`meta`, or a `setting`'s kind), sizes and timing. Never a message body, a name, a topic, a setting's
|
|
1425
|
+
content or a key.
|
|
1426
|
+
|
|
1295
1427
|
**What the relay can see.** Team ids, machine ids, persons, session names, timing, and presence:
|
|
1296
1428
|
its session names, agent kinds, states and tiers, and the repo, branch and doing text a machine
|
|
1297
1429
|
chooses to share (§6.5, none by default). It never sees message, task or receipt bodies.
|
|
@@ -1355,6 +1487,63 @@ chooses to share (§6.5, none by default). It never sees message, task or receip
|
|
|
1355
1487
|
|
|
1356
1488
|
### 8.2 IPC API
|
|
1357
1489
|
|
|
1490
|
+
**Conversation render schemas.** Core exports `ParticipantView`, `NoticeView`, `FileView`, `TaskView`, `EventView`, `ConvView`, `ActivityBody` and `ConvStreamEvent` (§16).
|
|
1491
|
+
Names and content are untrusted peer data. A `FileView` exposes no attachment key or nonce.
|
|
1492
|
+
A deleted `EventView` contains neither text nor files. Lane 4 omits read positions, preferences and unread counts.
|
|
1493
|
+
Stream records carry a daemon-local `pos`. An `event.update` names the conversation and event of its embedded projection.
|
|
1494
|
+
Stream record types are `event`, `event.update`, `conv`, `member`, `read`, `typing`, `activity`, `gap` and `task` (person-level readers only, "The person's tasks").
|
|
1495
|
+
An `event.update` change is one of `edit`, `delete`, `react`, `pin`, `replies`, `delivery` and `task`; lane 4 emits `edit`, `delete`, `react`, `pin` and `delivery`.
|
|
1496
|
+
A `request.waiting` item's state is one of `held`, `pending-delivery`, `resend` and `outdated`.
|
|
1497
|
+
The daemon re-reads verified state before it sends a projection.
|
|
1498
|
+
|
|
1499
|
+
**Action tokens.** A human-tier client acts on an item it shows through an action the daemon put on that item:
|
|
1500
|
+
`{name, token}`, where `token` is an `ActionToken` (32 random bytes, base64url, 43 characters).
|
|
1501
|
+
- **Minting.** The daemon mints a token when it renders an item on `run/admin.sock` for a call that carries no session
|
|
1502
|
+
key. Under a Jelly host (§8.11) that call is under a method grant: a host-scope grant renders no token. The agent tier
|
|
1503
|
+
never receives one.
|
|
1504
|
+
- **Binding.** A token is for one action `name`, one target and one view. The target is the request (`request.list`)
|
|
1505
|
+
or the task (`TaskView`). The view is the admin connection that received the token, and under a Jelly host also the
|
|
1506
|
+
`dev` and `mbr` of the method grant. A missing `dev` or `mbr` is bound as missing.
|
|
1507
|
+
- **Life.** A token is spent by its first presentation, whether or not it matches. It lives 10 minutes. The daemon holds
|
|
1508
|
+
tokens in memory only, so a restart invalidates every one; the store does not change. A connection's tokens end
|
|
1509
|
+
when it closes.
|
|
1510
|
+
- **Limits.** A view that reads the same item again gets the same token while more than half of the token's life is
|
|
1511
|
+
left. A view holds at most 256 tokens and the daemon at most 4,096. Past either limit the oldest token ends.
|
|
1512
|
+
- **Redemption.** Only through the action's own method, with the token as the `token` param: a request's answer
|
|
1513
|
+
through `request.answer {id, answer, token}`, where `answer` is the action's `name`; a task's `cancel` through
|
|
1514
|
+
`task.cancel {taskId, token}`. The params name the target. Only `run/admin.sock` takes a token, for a call with no
|
|
1515
|
+
session key: elsewhere the call is `forbidden`, and that refusal does not spend the token. A token that is forged,
|
|
1516
|
+
spent, expired or ended, for another name or target, or from another view or an earlier start, is `grant_invalid`,
|
|
1517
|
+
and the call does nothing.
|
|
1518
|
+
- **Logging.** The daemon logs each refusal as `action.token.refused` with the method and a fixed reason. It never logs
|
|
1519
|
+
a token.
|
|
1520
|
+
- **Where tokens are offered.** `request.list` gives each request `actions`, one per answer its kind takes, while the
|
|
1521
|
+
request can be answered: an ask `once`, `always`, `decline` and `block`; a pending message request `accept`,
|
|
1522
|
+
`decline` and `block`; a suggestion `decline`; an agent invitation `accept` and `decline`. `TaskView.cancel` is
|
|
1523
|
+
offered only on a task that this person's agent asked (the requester side) and that has not ended. The executor side
|
|
1524
|
+
never gets `cancel`. No retry method exists for a failed send (the outbox retries by itself), so a failed send has
|
|
1525
|
+
no recovery action.
|
|
1526
|
+
- **Vectors.** `docs/spec/vectors/action-tokens.json` has requests with actions and the params that redeem them, with
|
|
1527
|
+
negative cases. `conversation-views.json` has a `TaskView` with `cancel`.
|
|
1528
|
+
|
|
1529
|
+
**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`.
|
|
1530
|
+
|
|
1531
|
+
**Live conversation methods.** `LIVE_CONVERSATION_METHODS` is part of `IPC_METHODS`. The tier table applies on both sockets.
|
|
1532
|
+
Batch `people.get` accepts up to 100 distinct person IDs and returns `{people, missing}` without device lists.
|
|
1533
|
+
`people.get` and `sessions.list` work without a connected conversation engine.
|
|
1534
|
+
Human-only `request.waiting` reports authenticated outgoing holds separately from pending delivery. `request.list` reports incoming requests.
|
|
1535
|
+
A `conv.send` names exactly one of `cid` and `to`. `broadcast` requires a thread.
|
|
1536
|
+
A page chooses at most one position. Agent calls require a session registered on their connection.
|
|
1537
|
+
The daemon enforces access, audience consent and own-line restrictions after asynchronous work too.
|
|
1538
|
+
Creation requires two DM participants or 3–50 group participants. Only channels have visibility. Only agents can be seated.
|
|
1539
|
+
`conv.access` pages accessible conversations. `conv.sync` waits at most 25 seconds and returns at most 50 events.
|
|
1540
|
+
A person-level `conv.sync` (admin socket, no key) also carries `task` entries for the person's tasks (§8.2, "The person's tasks").
|
|
1541
|
+
A restart or expired replay position produces a gap. Clients then reload their accessible conversations.
|
|
1542
|
+
Task changes share the replay buffer (its last 10,000 changes) with conversation changes, so a burst of task
|
|
1543
|
+
updates moves the oldest replayable position forward for every reader, keyed or not; a reader past it gets `gap`.
|
|
1544
|
+
`conv.backfill` returns `complete: false` until indexing coverage accounts for missing keys and expired entries.
|
|
1545
|
+
The method schemas and their error arrays are normative. Malformed JSON-RPC and params use JSON-RPC errors.
|
|
1546
|
+
|
|
1358
1547
|
The API is the method table `IPC_METHODS` in `schema/ipc.ts`: JSON-RPC 2.0 NDJSON on two Unix
|
|
1359
1548
|
sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (for example
|
|
1360
1549
|
`contacts.list`, `item.read` and `task.get`), the TSDoc in that file and in `schema/ipc/*.ts` is normative.
|
|
@@ -1367,8 +1556,8 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1367
1556
|
- **Two sockets.** The daemon serves the one API on two sockets, and itself enforces what each
|
|
1368
1557
|
serves (its tiers). A client's own method list is a second check, not the boundary.
|
|
1369
1558
|
- `run/agent.sock` (the agent tier) serves only `AGENT_METHODS`. Any other method fails with
|
|
1370
|
-
`forbidden` before its params are read.
|
|
1371
|
-
|
|
1559
|
+
`forbidden` before its params are read. Conversation calls on this socket require a bound session.
|
|
1560
|
+
They cannot send as the person. Agent-facing clients connect here: the OpenCode plugin's server part
|
|
1372
1561
|
and `messaging-mcp`.
|
|
1373
1562
|
- `run/admin.sock` (the admin tier) serves every method. Human clients connect here: the `messaging`
|
|
1374
1563
|
CLI and the `/messaging` TUI.
|
|
@@ -1422,7 +1611,7 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1422
1611
|
- **What the tiers are, and what they are not.** Where the daemon, OpenCode and the user's shell
|
|
1423
1612
|
run as one OS user, as on a laptop, the tiers are not a security boundary: any process of that
|
|
1424
1613
|
user can open `run/admin.sock`. They keep `team.*`, `identity.rotateEnc`, `daemon.stop`,
|
|
1425
|
-
|
|
1614
|
+
conversation administration and the person's own chat lines off every tool path. A tool that
|
|
1426
1615
|
speaks to the agent socket cannot reach them. A shell that runs the `messaging` CLI is still a shell,
|
|
1427
1616
|
and only the host's shell permissions govern it. The tiers are a boundary only when `messagingd`
|
|
1428
1617
|
runs as its own user and the engine runs as another user in `ipc.agentGroup` (the layout is in
|
|
@@ -1431,23 +1620,25 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1431
1620
|
as them and call every method, including `team.*` and `daemon.stop`. Agent-facing clients still
|
|
1432
1621
|
must not expose `team.*`. The OpenCode TUI is a human surface and may call them. It must not
|
|
1433
1622
|
place an invite ticket in a session, a log, or a model context.
|
|
1434
|
-
- Key-scoped methods (`session.update`, `session.unregister`, `events.subscribe`, `
|
|
1435
|
-
`
|
|
1436
|
-
|
|
1623
|
+
- Key-scoped methods (`session.update`, `session.unregister`, `events.subscribe`, `inbox.*`, `item.read`, `task.*` (but `tasks.list` and `tasks.get`),
|
|
1624
|
+
`conv.list`, `conv.read`, `conv.send`, `conv.react`, `conv.edit`, `conv.delete`, `conv.search`, `conv.context`, `conv.access`, `conv.sync`)
|
|
1625
|
+
work only for a key that the calling connection registered. Human conversation calls can omit the key on the admin socket.
|
|
1437
1626
|
- A session key is therefore a bearer credential for its session: whoever knows it can register it
|
|
1438
1627
|
and act as that session.
|
|
1439
1628
|
- `AGENT_METHODS` (with its type `AgentMethod`) lists the methods an agent-facing client may call,
|
|
1440
1629
|
and exactly the methods `run/agent.sock` serves. Anything else is for human clients: the
|
|
1441
1630
|
`messaging` CLI and the `/messaging` TUI.
|
|
1442
|
-
- **Tiers.**
|
|
1631
|
+
- **Tiers.** Conversation methods available to agents also appear in the human list:
|
|
1443
1632
|
`AGENT_METHODS` (the agent tier: sessions, `device.status`, `device.bind`, `contacts.list`,
|
|
1444
|
-
`
|
|
1633
|
+
`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.create`, `task.continue`, `task.list`, `task.get`, `task.update`, `task.cancel`, `task.question`,
|
|
1445
1634
|
`events.subscribe`, `daemon.status`, `daemon.health`); `HUMAN_METHODS` (acting as the person: `conv.list`,
|
|
1446
|
-
`conv.read`, `conv.
|
|
1447
|
-
`
|
|
1448
|
-
`
|
|
1449
|
-
`
|
|
1450
|
-
`
|
|
1635
|
+
`conv.read`, `conv.send`, `conv.react`, `conv.edit`, `conv.delete`, `conv.search`, `conv.context`, `conv.access`, `conv.sync`,
|
|
1636
|
+
`conv.get`, `conv.members`, `conv.threads`, `conv.pin`, `conv.create`, `conv.add`, `conv.remove`, `conv.role`, `conv.seat`,
|
|
1637
|
+
`conv.settings`, `conv.meta`, `conv.archive`, `conv.leave`, `conv.join`, `conv.accept`, `conv.policy`, `conv.backfill`, `people.search`, `people.get`,
|
|
1638
|
+
`grant.list`, `grant.set`, `grant.set.preview`, `reach.get`, `reach.set`, `reach.set.preview`, `request.list`, `request.answer`, `request.answer.preview`, `contact.request`,
|
|
1639
|
+
`contact.request.preview`, `card.get`, `sessions.list`, `request.waiting`,
|
|
1640
|
+
`notice.subscribe`, `notice.list`, `notice.ack`, `tasks.list`, `tasks.get`, `device.approve.preview`,
|
|
1641
|
+
`device.remove.preview`, `identity.replace.preview`, `directory.erase.preview`); and `ADMIN_METHODS`
|
|
1451
1642
|
(this device and the daemon: `daemon.stop`,
|
|
1452
1643
|
`daemon.woke`, `person.attached`, `daemon.snapshot`, `wake.arm`, `wake.disarm`,
|
|
1453
1644
|
`device.list`, `device.remove`, `device.link`, `device.login`, `device.login.status`, `device.login.cancel`,
|
|
@@ -1482,6 +1673,12 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1482
1673
|
Only a directory realm has it (`invalid_state` in a roster realm).
|
|
1483
1674
|
- `device.list` (admin tier) lists this device's person's devices, current first, with vouch, kind,
|
|
1484
1675
|
and for a removed one when, why and by which device it was unbound.
|
|
1676
|
+
- Each entry carries `features`: the presence features (§6.5) that device advertises. This device lists its own.
|
|
1677
|
+
Another device of the person lists those of the last presence this device verified from it.
|
|
1678
|
+
- `features` is absent when no presence of that device has been seen. That means unknown, not none.
|
|
1679
|
+
- A client warns while any current device of the person lacks a feature it relies on. The plugin does this for
|
|
1680
|
+
`reach`, the reach choices of §6.9: a device without it does not enforce them.
|
|
1681
|
+
- Only the person's own devices are listed. No method shows another person's devices or their features.
|
|
1485
1682
|
- `device.remove {device, reason?, token?}` (admin tier) unbinds this device (`self`), or another
|
|
1486
1683
|
current device of its person (`removed` by default, or `lost`) with a fresh Jelly token bound to this
|
|
1487
1684
|
device (`invalid_request` without one). A directory realm only; a roster realm uses `team.remove`.
|
|
@@ -1562,12 +1759,8 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1562
1759
|
`PeerRef` (inbox items, task records), provenance (§6.6), directory entries (whose client kind is
|
|
1563
1760
|
`client`) and chat lines. `daemon.status` names this device in `identity` (its `device`), and a divergence alarm
|
|
1564
1761
|
the signer's `device`. Team administration (`team.*`) keeps the roster's `machine`.
|
|
1565
|
-
- **Persons in the API.**
|
|
1566
|
-
|
|
1567
|
-
`contacts.list`, `people.*`, a `dm:` or `agent:` conversation, and a line's `to` in `msg.send`), it
|
|
1568
|
-
takes a `PersonId`: a roster person name or a hosted `p_…` id. Channel membership (`channel.*`, and the
|
|
1569
|
-
`persons` and `creator` of a channel op, §6.8) takes `PersonId`s too. Team administration (`team.*`) takes
|
|
1570
|
-
roster person names.
|
|
1762
|
+
- **Persons in the API.** Person references use `PersonId`: a roster person name or a hosted `p_…` ID.
|
|
1763
|
+
Conversation participants use person IDs or stable `person/agent` addresses. Team administration uses roster person names.
|
|
1571
1764
|
- **`session.register`.** Names a session.
|
|
1572
1765
|
- The requested name is suffixed `-2`, `-3`, ... if it is taken on this machine.
|
|
1573
1766
|
- Registering an existing key again updates it; this is idempotent.
|
|
@@ -1608,24 +1801,14 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1608
1801
|
expired `trustedUntil` is left out. So an agent sees whom it can reach without a human-tier call. A `reach` also
|
|
1609
1802
|
carries `devicesChangedAt` while a device of that person was added, removed or replaced less than 7 days ago, as
|
|
1610
1803
|
their verified log (or the roster) shows it; a rekey or an attest is not a change, and it carries no other text. Entries
|
|
1611
|
-
carry `expect`: the hint a `
|
|
1804
|
+
carry `expect`: the hint a `task.create` to that session, with the wake hint set, returns
|
|
1612
1805
|
(below), computed from the same view. Agent clients show it before anything is sent. It is absent
|
|
1613
1806
|
when no presence lists the session.
|
|
1614
|
-
- **`
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
at most `LIMITS.chatChars`, and `replyTo` as the thread (the `id` of any line of it); any other
|
|
1618
|
-
A2A field (`contextId`, `taskId`, `messageId`, `metadata`, `extensions`, `referenceTaskIds`,
|
|
1619
|
-
`onBehalfOf`, `wake`) is `invalid_request`, and so is a `to` that is neither a person
|
|
1620
|
-
name nor `#` and a channel name. It fails with `not_found` for a person not on the team, a
|
|
1621
|
-
channel this person is not in, or an unknown thread, and `too_large` for a longer line. It writes
|
|
1622
|
-
the line as `person.send` does (below), and `idempotencyKey` is the line's `clientId`. Its one
|
|
1623
|
-
result is `{to, envelopeId: <line id>, messageId, contextId, stage: "accepted"}`. With `request:
|
|
1624
|
-
{contact: <email>}`, only on a line to the session's own person (otherwise `invalid_request`), the daemon
|
|
1625
|
-
also files a suggestion for the person (§6.9, "Message requests").
|
|
1626
|
-
- **To `person/session[@label]`: mail or a task** to that session, as the rest of this item says.
|
|
1807
|
+
- **`task.create` and `task.continue`.** These methods retain task envelopes until lane 7.
|
|
1808
|
+
The destination is `person/session[@label]`. Creation assigns a task ID and signs a delegation chain.
|
|
1809
|
+
Continuation requires the existing task ID and its bound executor. Both require the calling session's `key`.
|
|
1627
1810
|
- Refuses content that holds a credential (`secret_refused`, §8.7), as do `task.update`,
|
|
1628
|
-
`task.cancel` and `
|
|
1811
|
+
`task.cancel`, `conv.send` and `conv.edit`.
|
|
1629
1812
|
- Resolves the address against the directory, including offline sessions.
|
|
1630
1813
|
- Builds one envelope per recipient and stores it in the outbox (`accepted`).
|
|
1631
1814
|
- Sends it when the relay is connected.
|
|
@@ -1651,14 +1834,12 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1651
1834
|
`idempotencyKey` returns the hint computed for the original call.
|
|
1652
1835
|
- `idempotencyKey` returns the original results for a repeated call from the same key.
|
|
1653
1836
|
- `wake` defaults to true.
|
|
1654
|
-
-
|
|
1655
|
-
- A `kind: "task"` send signs a root grant for each recipient (§6.7).
|
|
1837
|
+
- Creation assigns ULID task IDs and a `contextId` when omitted, and signs a root grant for each recipient (§6.7).
|
|
1656
1838
|
- A chain this daemon already knows the recipient would refuse is not sent. The result is
|
|
1657
1839
|
`stage: "rejected"` with that check's code, and the requester's task is stored
|
|
1658
1840
|
`TASK_STATE_REJECTED` with no grants.
|
|
1659
1841
|
- **`onBehalfOf {taskId, scope?, maxDepth?}`** delegates on behalf of a task that the calling key
|
|
1660
|
-
**executes**, and only with
|
|
1661
|
-
or `invalid_request` (`kind` is not `"task"`), before anything is stored. The daemon:
|
|
1842
|
+
**executes**, and only with creation. Any other task fails with `not_found` before storage. The daemon:
|
|
1662
1843
|
1. refuses a terminal task (`invalid_state`) before anything is stored;
|
|
1663
1844
|
2. signs the next grant of the stored chain for each recipient, narrowed to `scope` and
|
|
1664
1845
|
`maxDepth` when given;
|
|
@@ -1678,104 +1859,132 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1678
1859
|
`invalid_state`.
|
|
1679
1860
|
- The daemon validates the transition and stores it.
|
|
1680
1861
|
- It then emits `task.status` and/or `task.artifact`.
|
|
1681
|
-
- **`task.cancel`** is allowed only for the requester
|
|
1862
|
+
- **`task.cancel`** is allowed only for the requester: the requester session (`{key, taskId, reason?}`), or, on the
|
|
1863
|
+
human tier, the person with the task's `cancel` action (`{taskId, token, reason?}`, "Action tokens"). It takes
|
|
1864
|
+
exactly one of `key` and `token`. A cancel by token returns the record with the owner view, as the admin socket's
|
|
1865
|
+
task reads do.
|
|
1866
|
+
- **The task view.** Every `TaskRecord` the daemon projects (`task.get`, `task.list`, `task.update`, `task.cancel` and
|
|
1867
|
+
`task` events) carries fields it computes as it projects the record. They are never stored and never sent to the
|
|
1868
|
+
peer:
|
|
1869
|
+
- `agent`: the counterpart agent, the session its verified envelopes name (`peer.agent`).
|
|
1870
|
+
- `person: {id, name}`: that agent's person, `peer.person`, with the display name the realm verified for them (the
|
|
1871
|
+
person id when the realm does not know them). It is never parsed from an agent's name.
|
|
1872
|
+
- `org`: `same`, `other` or `none`, that person's relation to this device's person, as `ParticipantView.org` computes
|
|
1873
|
+
it. (Jelly calls it `organisation`; the plugin maps the name.)
|
|
1874
|
+
- `person` and `org` are absent while the daemon cannot name the person yet: a `task` event is projected at once from
|
|
1875
|
+
what it last learned, and a read looks the person up first.
|
|
1876
|
+
- `signIn: {tool?, by}`, in `TASK_STATE_AUTH_REQUIRED` only. `by` is `you` when the executor is an agent of this
|
|
1877
|
+
device's person (this device runs the task, or the peer is another device of the same person), else `them`. The
|
|
1878
|
+
viewer's own daemon computes it; it is never read from the peer. `tool` is what the executor named (§6.2,
|
|
1879
|
+
"Sign-in"): plain text through `cleanPeerText`, single line, at most `TASK_SIGN_IN_TOOL_CHARS`
|
|
1880
|
+
(`LIMITS.previewChars`, 200) characters; absent when it named none.
|
|
1881
|
+
- `refusal: {reason}`, in `TASK_STATE_REJECTED` only, when the executor gave words: the text of its status message,
|
|
1882
|
+
through `cleanPeerText`, at most `TASK_REFUSAL_CHARS` (`LIMITS.requestChars`, 500) characters.
|
|
1883
|
+
- The tool and the reason are only ever the executor's own words: on the executor's side, its session's
|
|
1884
|
+
`task.update`; on the requester's, a status message received from the executor (it carries the provenance the
|
|
1885
|
+
daemon wrote, §6.6). A status the requester's daemon made itself, such as "delivery rejected: refused", gives
|
|
1886
|
+
neither.
|
|
1887
|
+
- Both are untrusted peer text, as `InboxItem.preview` is: a UI shows them as plain text, never markup, and a model
|
|
1888
|
+
sees them only fenced (§11). So is the `name` in `person`: the realm verifies who the person is, but the name is their own
|
|
1889
|
+
choice, and a UI shows it as plain text.
|
|
1890
|
+
- `question: {sessionId, approvalId}`: the executor host's own pending approval or question that an
|
|
1891
|
+
`AUTH_REQUIRED` or `INPUT_REQUIRED` task waits on. Both ids are the host engine's, opaque plain identifiers
|
|
1892
|
+
(`[A-Za-z0-9][A-Za-z0-9._:-]{0,199}`).
|
|
1893
|
+
- **Who sets it.** The session that runs the task: its client's runtime calls agent-tier
|
|
1894
|
+
`task.question {key, taskId, question}` from its host's own permission and question events (`question: null`
|
|
1895
|
+
unties it). It is never a model tool.
|
|
1896
|
+
- **What is checked.** `key` must be registered on the calling connection (`not_found`). The task must be one that
|
|
1897
|
+
session executes: another session's task, or an unknown one, is `not_found`, and the requester side is
|
|
1898
|
+
`invalid_state`. The task must be in one of those two states (`invalid_state`). When the session's registration
|
|
1899
|
+
named its host session (`session.register {host: {sessionID}}`), `sessionId` must equal it (`forbidden`).
|
|
1900
|
+
- **The limit of that check.** A registration without `host` (under a Jelly host, one key serves all of an
|
|
1901
|
+
agent's sessions) gives the daemon no host session to compare. Then `sessionId` is the runtime's word, scoped
|
|
1902
|
+
to that agent's own task.
|
|
1903
|
+
- **No human-tier form.** Only the executing session knows which approval its task waits on. An admin-socket
|
|
1904
|
+
client could name any approval of any task, with nothing to check it against.
|
|
1905
|
+
- The daemon keeps it **in memory only**: the approval it names does not survive a restart, so neither does the
|
|
1906
|
+
association. Any later status of the task (a `task.update`, a cancel from the requester, the 24 h timeout)
|
|
1907
|
+
unties it.
|
|
1908
|
+
- Setting it from the agent tier does not make it visible there. It is shown only on the admin socket (the
|
|
1909
|
+
person's own clients): in `tasks.list`, `tasks.get` and `conv.sync`'s `task` entries, and in `task.get`,
|
|
1910
|
+
`task.list`, `task.update` and `task.cancel` results there, for the executor's side of a task still in one of
|
|
1911
|
+
those states. It is never on the
|
|
1912
|
+
agent socket, never in a `task` event (events go to both sockets), and never in anything sent to the peer.
|
|
1913
|
+
- Nothing in it travels: the peer's daemon cannot learn the host's session or approval ids.
|
|
1914
|
+
- **The person's tasks.** `tasks.list {role?, state?, session?, cursor?, limit?}` and `tasks.get {taskId, role?}` are
|
|
1915
|
+
human tier (admin socket only, no session key). They read the tasks this person's agents on this device are in,
|
|
1916
|
+
as requester or executor, across all their sessions. They return `PersonTask`: the task view above, as the owner
|
|
1917
|
+
sees it, `question` included.
|
|
1918
|
+
- In place of the session `key` (a bearer credential), a `PersonTask` names the agent in `session`. The schema
|
|
1919
|
+
refuses a `key`.
|
|
1920
|
+
- On the requester side of a task that has not ended, it carries `cancel`, minted for this view as `request.list`'s
|
|
1921
|
+
actions are ("Action tokens"). The executor side never has one.
|
|
1922
|
+
- A device holds only its own sessions' sides, so another person's tasks are never listed, nor the person's other
|
|
1923
|
+
devices' tasks.
|
|
1924
|
+
- `tasks.list` is most recently updated first, 50 by default and at most 100 per page. A page also stops at the list
|
|
1925
|
+
byte budget. It reports `total` and pages with an opaque `cursor`/`nextCursor`. `session` filters to one agent by
|
|
1926
|
+
name; an unknown name lists nothing. `tasks.get` needs `role` only when this device holds both sides (one of its
|
|
1927
|
+
agents asked another); without it, `invalid_request`. An unknown task is `not_found`.
|
|
1928
|
+
- **Live.** A change of such a task (any `task` event, and `task.question`) adds a `task` entry to `conv.sync`'s
|
|
1929
|
+
stream (§12.4): `{type: "task", pos, task: PersonTask}`, rendered for each reader when it reads, so `cancel` is
|
|
1930
|
+
minted for that reader's view. Only a person-level reader gets these entries: the admin socket with no session
|
|
1931
|
+
key. The agent tier, and any `conv.sync` with a `key`, skip them. The stream is chosen over a new push kind so a
|
|
1932
|
+
host's request-and-response section stays live with the long poll it already makes (design §12.1). When one read
|
|
1933
|
+
covers several changes of the same side, it renders that side once, as it is now.
|
|
1934
|
+
- Action class `view` under a Jelly host (§8.11).
|
|
1682
1935
|
- **`task` events** carry `origin`:
|
|
1683
1936
|
- `local` when the event follows the subscriber key's own call: `task.update`, `task.cancel`,
|
|
1684
|
-
or the `
|
|
1937
|
+
or the `task.create` call that created the record;
|
|
1685
1938
|
- `peer` when it follows an envelope from the other side, a receipt, or a daemon timeout.
|
|
1686
1939
|
- When the event follows a status or artifact update, `update` carries that delta, including
|
|
1687
1940
|
`append` and `lastChunk`.
|
|
1688
|
-
- **`conv.read {key?,
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
"agent"`), which needs the session `key`: what the session `key` exchanged with one other session,
|
|
1692
|
-
on any machine of that person: incoming mail,
|
|
1693
|
-
outgoing mail and tasks either way, as `ConvEntry` rows in time order. A task request appears
|
|
1694
|
-
once, as its task (dated when it was created, with its current state), not again as mail. A
|
|
1695
|
-
cursor is an entry's `{at, id}` with an agent, and a line's `seq` in a DM or a channel; the other
|
|
1696
|
-
form is `invalid_request`, and so is `thread` with an agent. With no cursor it is the latest `limit` entries (default 30, at
|
|
1697
|
-
most 100); with `before` the latest `limit` before that place; with `after` the earliest `limit`
|
|
1698
|
-
after it. Entries come oldest first, and `more` says entries remain past the page in the
|
|
1699
|
-
direction read; `before` and `after` together are invalid params. Incoming mail carries its
|
|
1700
|
-
preview and stage, a task the start of its request, the session's role and the task's state.
|
|
1701
|
-
Outgoing mail carries its stage but no text: the daemon keeps no copy of what it sent. It changes
|
|
1702
|
-
no stage and sends no receipt. Agent clients build their conversation view on it (§11).
|
|
1941
|
+
- **`conv.read {key?, cid, thread?, before?, after?, around?, limit?, cursor?}`** returns current event projections, oldest first, or an explicit context hold.
|
|
1942
|
+
Pages contain at most 50 events. Before and after positions are log sequence numbers. An opaque cursor binds to the query and session.
|
|
1943
|
+
Reads expose no legacy mail or fabricated task events. Retained tasks use their own methods.
|
|
1703
1944
|
- **`task.list`:** most recently updated first. It filters by `role`, `state`, `contextId`,
|
|
1704
1945
|
`peer` (a qualified address) and `updatedAfter` (at or after, ≥), pages with an opaque `cursor`/`nextCursor`, and reports `total`.
|
|
1705
|
-
-
|
|
1946
|
+
- **Task A2A passthrough:** `messageId` (generated if omitted), `metadata` (at most
|
|
1706
1947
|
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
|
|
1707
1948
|
side), `extensions`, and `referenceTaskIds`.
|
|
1708
|
-
- **
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
- `conv.markRead {conversation, upTo}` moves the conversation's read cursor to `upTo`, a line's `seq`, if that is
|
|
1720
|
-
later, clamped to the newest `seq` here. It returns `{readUpTo, unread}`. It is a human-client
|
|
1721
|
-
method that is not in `AGENT_METHODS`: read state is the person's, and an agent reading a
|
|
1722
|
-
conversation does not mark it read. It fails with `not_found` for a channel this person is not in.
|
|
1723
|
-
- A `ChatLine` this machine sent to other persons carries `state` (`sent`, `delivered` or
|
|
1724
|
-
`refused`) and, when refused, `refused` (the persons). A line the daemon wrote itself, about a
|
|
1725
|
-
channel change or a refusal of one, carries `notice: true`.
|
|
1726
|
-
- `person.send {conversation, text, thread?, clientId?}` (human tier) writes the person's own line
|
|
1727
|
-
and its envelopes in one transaction and sends them at once, with no session. It fails with
|
|
1728
|
-
`not_found` for a person not on the team or a channel this person is not in. A DM to oneself
|
|
1729
|
-
is the person's own conversation (§6.8).
|
|
1730
|
-
- `search {query, conversation?, limit?}` finds lines in the DMs and channels this person holds,
|
|
1731
|
-
newest first, as `hits`, each with its `conversation`.
|
|
1732
|
-
- **Pacing.** The sender keeps its chat envelopes to one recipient machine below that machine's
|
|
1733
|
-
chat budget (§8.3): the reference daemon allows 50 a minute, ten below
|
|
1734
|
-
`LIMITS.inboundPerMachinePerMinute`, for clock and transit jitter. Channel changes count too. A
|
|
1735
|
-
line (`person.send`, or `msg.send` to a person or a channel) that would go over it for any machine
|
|
1736
|
-
it goes to fails with `rate_limited`, a
|
|
1737
|
-
message that says when to try again, and nothing stored or sent. Pacing cannot cover a backlog:
|
|
1738
|
-
lines queued at the relay for an offline machine reach it together when it connects. The
|
|
1739
|
-
receiver defers those past its budget and the relay paces them (§7 "Delivery", §8.3), so every
|
|
1740
|
-
line is delivered within minutes; only one that would expire first is refused.
|
|
1741
|
-
- A line with a `clientId` (`msg.send`'s `idempotencyKey`) is idempotent per `clientId` and
|
|
1742
|
-
sender: the session `key`, or the person for `person.send`. A repeat with the same room, thread and text returns the first call's
|
|
1743
|
-
result and files and sends nothing. The same `clientId` for another line is `invalid_request`.
|
|
1744
|
-
The daemon keeps a `clientId` for `LIMITS.ttlMs`.
|
|
1745
|
-
- `channel.create`, `channel.invite`, `channel.leave` and `channel.remove` are for the human
|
|
1746
|
-
`/messaging` TUI. They take a channel name as this machine holds it (its local name). Each checks
|
|
1747
|
-
before it stores anything. `channel.create` refuses a name already held, more than the
|
|
1748
|
-
channels a machine holds (`invalid_request`), an unknown person, or more than
|
|
1749
|
-
`LIMITS.channelPersons` persons with the creator. `channel.remove` refuses a caller who is not
|
|
1750
|
-
a member (`not_found`), who is neither the creator nor a team admin (`forbidden`), a list that
|
|
1751
|
-
names the caller (`invalid_request`: use `channel.leave`), and a person who is not a member
|
|
1752
|
-
(`not_found`).
|
|
1753
|
-
- Mail and tasks (`msg.send` to an agent) never write the transcript.
|
|
1754
|
-
- **`conv` events** go only to connections that called `conv.subscribe`, a human-client method
|
|
1755
|
-
that is not in `AGENT_METHODS`. They are never sent to `events.subscribe` keys. A `conv` event
|
|
1756
|
-
names the conversation, the line's `id` and its person, never its text. It is raised for a filed line,
|
|
1757
|
-
a channel change, and a change of an outgoing line's delivery state (then `person` is this
|
|
1758
|
-
machine's person). Each carries the `seq` of its change (§6.8), so events rise in `seq`.
|
|
1759
|
-
- **Resuming.** `conv.subscribe {since}`, where `since` is the `seq` of the last event the client
|
|
1760
|
-
handled, first sends one `conv` event per line that changed after `since`, oldest change first,
|
|
1761
|
-
each with the `seq` of the line's latest change. Then live events follow, with none between.
|
|
1762
|
-
It returns `{seq, gap}`: the newest `seq` at the call, and whether the replay could not be
|
|
1763
|
-
complete, in which case it sends none. That is when more than a policy number of lines changed
|
|
1764
|
-
(the reference daemon: 500), a channel was deleted here after `since`, or `since` is above
|
|
1765
|
-
`seq` (a store set up again). After a gap the client reloads with `conv.list` and `conv.read`.
|
|
1766
|
-
Without `since`, `gap` is false and no events are replayed.
|
|
1949
|
+
- **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`.
|
|
1950
|
+
Every agent call uses its bound session. Human calls use the admin socket.
|
|
1951
|
+
A `conv.send` returns `{cid, id, clientId, stage: "pending"}` or an explicit audience hold.
|
|
1952
|
+
The durable outbox retries identical signed events. A repeated `clientId` returns the first event ID.
|
|
1953
|
+
A queued key share whose recipient device the conversation's own verified log removes (an accepted `remove` of its
|
|
1954
|
+
person or the person's own `leave`, with the person no longer a participant, or a `rekey {cause: device}` naming
|
|
1955
|
+
the device) is dropped, never sent, and logged by conversation, device and event id. Any other share to a device
|
|
1956
|
+
that is no longer current, such as a directory miss, holds the outbox until the state is verified.
|
|
1957
|
+
Reuse with different content or a different destination fails with `invalid_request`.
|
|
1958
|
+
Conversation administration uses the human-tier methods in the tier list. Internal notifications carry identifiers only.
|
|
1959
|
+
The stream re-renders current verified state, including edits and deletions, for each reader.
|
|
1767
1960
|
- **Consent (§6.9).** `grant.list`, `grant.set`, `grant.set.preview`, `request.list`,
|
|
1768
1961
|
`request.answer`, `request.answer.preview`, `contact.request`, `contact.request.preview` and `card.get` are human-client methods that are
|
|
1769
1962
|
not in `AGENT_METHODS`: agents never read or change a person's grants, answer a held request, or send a message
|
|
1770
|
-
request.
|
|
1963
|
+
request. So are `reach.get`, `reach.set` and `reach.set.preview`: agents never read or change the person's reach
|
|
1964
|
+
choices.
|
|
1965
|
+
- `reach.get {}` (read-only) returns `{revision, available, groups}`: `available` is true; `groups` are the two
|
|
1966
|
+
questions of "Reach choices" (§6.9), in order, each `{id, label, options, value, lapsed?}`. `id` is `message`
|
|
1967
|
+
("Who can message you") or `ask` ("How agents may ask yours"); `options` are `{value, label, description}` for
|
|
1968
|
+
`open` (Open), `ask` (Ask me first) and `never` (Never), in that order, in fixed words (`REACH_QUESTIONS`);
|
|
1969
|
+
`value` is the person's choice, from the relation grant this device holds whose signer is current, or null for
|
|
1970
|
+
none (the defaults apply); `lapsed` is a choice that lapsed with a removed signer and waits on the review list.
|
|
1971
|
+
`revision` is opaque, and changes whenever a relation grant or its review entry changes on this device, also by
|
|
1972
|
+
sync from another device or a re-sign.
|
|
1973
|
+
- `reach.set {revision, changes}` takes the `revision` the caller read and `changes`, an object of at most the two
|
|
1974
|
+
group ids, each one of the three values: anything else is invalid params. When `revision` is not the current
|
|
1975
|
+
one it fails with `conflict` and changes nothing. Otherwise it signs, for each change, the relation grant of
|
|
1976
|
+
"Reach choices" with this device, stores it, takes the subject off the review list and sends it to this person's
|
|
1977
|
+
other devices, all in one transaction, and returns the new snapshot (`reach.get`'s result). An empty `changes`
|
|
1978
|
+
changes nothing. Under a Jelly host it is a `local` action (§8.11).
|
|
1979
|
+
- `reach.set.preview` takes the same params, checks them the same way (`conflict` too), changes nothing, and
|
|
1980
|
+
returns one fixed `sentence` saying what the changes would do, for a confirmation.
|
|
1771
1981
|
- `grant.list {subject?}` returns this person's grants as this device holds them, newest first,
|
|
1772
1982
|
each with its levels, `id`, `ts`, `by` and, when re-signed, `resignedAt`, and the review list:
|
|
1773
|
-
the widening levels that lapsed with their signer, with when they lapsed. A
|
|
1774
|
-
carries the channel id and, while this person is in it, its local name.
|
|
1983
|
+
the widening levels that lapsed with their signer, with when they lapsed. A conversation subject carries its canonical `cid`.
|
|
1775
1984
|
- `grant.set {subject, chat?, agentMail?, ask?, trustedUntil?, trust?, discoverable?}` signs the
|
|
1776
1985
|
whole new grant for the subject with this device: a level left out is the relation's default,
|
|
1777
|
-
and no level at all is a clear. `subject` is `{person}` or `{
|
|
1778
|
-
|
|
1986
|
+
and no level at all is a clear. `subject` is `{person}` or `{conv: cid}`.
|
|
1987
|
+
`trust: true` without `trustedUntil` trusts for `LIMITS.trustDefaultMs`. It stores the
|
|
1779
1988
|
grant, takes the subject off the review list and sends the grant to this person's other devices
|
|
1780
1989
|
in one transaction. It fails with `not_found` for a person the realm does not know or a channel
|
|
1781
1990
|
this person is not in, `invalid_request` for a grant that breaks a rule of §6.9, and
|
|
@@ -1787,12 +1996,15 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1787
1996
|
`agent`, `address`), the local session it is for, an untrusted `preview`, when it was held and when it
|
|
1788
1997
|
expires; then the message requests (`contact`), each with its envelope `id`, the sender as the realm verified
|
|
1789
1998
|
them (`person`, `device`, `label`, `name`, `email?`, `org`), its untrusted `text`, `at` and `exp`; then the
|
|
1790
|
-
suggestions (`suggestion`), each with its `id`, the local `agent` that asked, the `email` and `at`.
|
|
1999
|
+
suggestions (`suggestion`), each with its `id`, the local `agent` that asked, the `email` and `at`. Each item
|
|
2000
|
+
also carries `actions` ("Action tokens").
|
|
1791
2001
|
- `request.answer {id, answer, all?}` answers one and returns the ids answered: an ask takes `once`, `always`,
|
|
1792
2002
|
`decline` or `block` ("Holds"), a message request `accept`, `decline` or `block` ("Message requests"), a
|
|
1793
2003
|
suggestion `decline`; any other answer is `invalid_request`. With `all`, the same answer goes to every request
|
|
1794
2004
|
of the same kind held from that person. It fails with `not_found` for an id this device does not hold, or one
|
|
1795
|
-
past its expiry.
|
|
2005
|
+
past its expiry. `token` is the request's action for this `answer`. It is checked before anything else, it never
|
|
2006
|
+
comes with `all`, and under a Jelly host it is required (`grant_required` without it). So under a Jelly host
|
|
2007
|
+
`all` is never used: each request is answered with its own token.
|
|
1796
2008
|
- `request.answer.preview {id, answer, all?}` (`request.answer`'s input) changes nothing and returns, for the
|
|
1797
2009
|
answer's confirmation, whom it reaches and what it does: `{kind, from: {name, email?, org}, agent?, to?, count,
|
|
1798
2010
|
sentence}`. `from` is the realm's verified profile of the sender (of the person themself for their
|
|
@@ -1800,7 +2012,8 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1800
2012
|
for, `count` how many requests the answer would settle (more than 1 only with `all`), and `sentence` one fixed
|
|
1801
2013
|
sentence saying what `answer` does. A name or email in the sentence is
|
|
1802
2014
|
cleaned to one line of at most 80 (100) characters. It refuses as `request.answer` would: `not_found` for an id
|
|
1803
|
-
this device does not hold or one past its expiry, `invalid_request` for an answer the kind does not take.
|
|
2015
|
+
this device does not hold or one past its expiry, `invalid_request` for an answer the kind does not take. It
|
|
2016
|
+
checks a `token` as `request.answer` does, but does not spend it.
|
|
1804
2017
|
- `contact.request {email, text, suggestion?}` (§6.9, "Message requests") resolves the exact `email` and sends
|
|
1805
2018
|
`text` (1 to `LIMITS.requestChars` UTF-16 code units) as a message request, and returns `{id, conversation,
|
|
1806
2019
|
person}`: the line's id, `dm:<person>` and the person found. It fails with `invalid_request` in a roster
|
|
@@ -1843,27 +2056,18 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1843
2056
|
`daemon.snapshot {done: true}` deletes it and returns `{released}` (false when none was held). One is held at a time
|
|
1844
2057
|
(`invalid_state` while one is); the daemon releases it after an hour (`until`), when it stops, and one a previous run
|
|
1845
2058
|
left when it starts.
|
|
1846
|
-
- **Your agents.** `
|
|
1847
|
-
|
|
1848
|
-
- `agents.list` returns each local session (registered or kept) as `{agent, conversations, unread,
|
|
1849
|
-
lastAt?, state}`: how many other agents it has mail or tasks with, the mail it has not read, its last
|
|
1850
|
-
traffic, and `idle`, `busy` or `kept`; most recent first.
|
|
1851
|
-
- `agents.read {agent, conversation?, before?, limit?}` returns, without `conversation`, `{kind:
|
|
1852
|
-
"conversations", conversations}`: each `agent:<person>/<agent>` it has traffic with, with its unread
|
|
1853
|
-
mail and last time, most recent first. With `conversation`, it returns `{kind: "entries",
|
|
1854
|
-
conversation, entries, more}`: a page of entries as `conv.read` gives them to that agent (default
|
|
1855
|
-
30, latest first, `before` an entry's `{at, id}`). It changes no stage and sends no receipt.
|
|
1856
|
-
`not_found` for an agent this device has no session of. Peer text is untrusted; a client cleans it
|
|
1857
|
-
for display (`cleanPeerText`) and never shows it to a model.
|
|
2059
|
+
- **Your agents.** Human-tier `sessions.list` returns local sessions as `{agent, state, seats}`.
|
|
2060
|
+
State is `idle`, `busy` or `kept`. Seats are verified conversation IDs. No unread count is fabricated.
|
|
1858
2061
|
- **Notices.** What the person is told about, in three fixed categories, as `{id, category,
|
|
1859
|
-
conversation?, from?: {name, email?, org}, line?, at}`:
|
|
1860
|
-
- `message`: a
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
2062
|
+
conversation?, from?: {name, email?, org}, line?, key?, at}`:
|
|
2063
|
+
- `message`: a retained legacy notice, without transcript text. Live conversation updates use the conversation stream.
|
|
2064
|
+
- `message` with `key: "task:<task id>"`: a task another person's agent asked of one of this person's agents. It is
|
|
2065
|
+
filed when this device accepts the task (when the `TaskRecord` of the executor's side is created), with `from` the
|
|
2066
|
+
requesting person. It copies no text of the task. It waits on the agent, not the person: it is never a `request`
|
|
2067
|
+
and never in `counts.requests`. An ask held for consent (§6.9, "Holds") is filed as a `request` and stays one until
|
|
2068
|
+
its person answers. If they let it through, the accepted task then files its `message` notice; if they decline,
|
|
2069
|
+
there is none. A task between this person's own agents files no notice. A host opens the notice by its `key`.
|
|
2070
|
+
- `request`: a held request or retained contact suggestion (§6.9). New notices contain no legacy room reference.
|
|
1867
2071
|
- `alert`: an alarm of this device (`keys_missing`, `restored`, `directory`), with fixed text in `line`; a
|
|
1868
2072
|
change of another device of the person (below), with `device`; or trust the person gave someone that ends
|
|
1869
2073
|
within 7 days (§6.9), with `from` that person and `line` "Trust you gave this person ends on <date>; renew it
|
|
@@ -1887,19 +2091,18 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1887
2091
|
admitted by the team's admin.", "'tablet' (other) on your account is now vouched by Jelly ID: ...", "'desk'
|
|
1888
2092
|
was replaced by 'desk2' (jelly-host), ...", "'phone' (laptop) was removed from your account.".
|
|
1889
2093
|
- Each notice is filed once, when its source is, under an id derived from the source (`m_<line id>`,
|
|
1890
|
-
`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:
|
|
1891
|
-
|
|
1892
|
-
through `cleanPeerText`, at most 120 characters with its "…".
|
|
2094
|
+
`m_task<task id>`, `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:
|
|
2095
|
+
`lines: true` can expose fixed alert text. It never restores legacy transcript content.
|
|
1893
2096
|
- `notice.subscribe {after?, lines?}` sends the unacknowledged notices filed after the notice `after`
|
|
1894
2097
|
(all of them without it, at most 500), oldest first, as `notice` events on this connection, then each
|
|
1895
2098
|
new one as it is filed; it returns `{replayed}`. `notice.list {lines?, limit?}` returns the
|
|
1896
|
-
unacknowledged notices, newest first (default 50), and `counts`:
|
|
1897
|
-
yet marked read over every conversation, and `requests`, the asks, message requests and suggestions held. `notice.ack {ids}` marks
|
|
2099
|
+
unacknowledged notices, newest first (default 50), and `counts.requests`: the asks, message requests and suggestions held. `notice.ack {ids}` marks
|
|
1898
2100
|
notices seen; unknown ids are ignored. It never answers, releases or uncounts a request: a request stays held, in
|
|
1899
2101
|
`request.list` and `counts.requests`, until `request.answer` (or its expiry). Notices go with the transcript's retention, and acknowledged ones
|
|
1900
2102
|
with the read mail's.
|
|
1901
2103
|
- **Previews.** Each preview takes exactly its action's input, so a host that draws a confirmation calls it with
|
|
1902
|
-
the input the action will get (Jelly does): `grant.set.preview` takes `grant.set`'s params, `
|
|
2104
|
+
the input the action will get (Jelly does): `grant.set.preview` takes `grant.set`'s params, `reach.set.preview`
|
|
2105
|
+
`reach.set`'s `{revision, changes}`, `request.answer.preview`
|
|
1903
2106
|
`request.answer`'s `{id, answer, all?}`, `contact.request.preview` `contact.request`'s `{email, text?, suggestion?}`,
|
|
1904
2107
|
`device.approve.preview` `device.approve`'s `{link, sas?}` and `device.remove.preview` `device.remove`'s `{device,
|
|
1905
2108
|
reason?, token?}`. A field the preview does not read may be left out (`text`, `sas`, `suggestion`, `reason`,
|
|
@@ -1908,7 +2111,7 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
|
|
|
1908
2111
|
changes nothing. What each returns is unchanged: `device.remove.preview` (human tier, read-only, a directory realm
|
|
1909
2112
|
only) returns what removing a device of this person would remove, `{label, kind?, vouch, current, self, since?}`,
|
|
1910
2113
|
or `not_found`; `device.approve.preview` returns `{device, label, kind, sas}` ("Devices"); `grant.set.preview`,
|
|
1911
|
-
`request.answer.preview` and `contact.request.preview` are under "Consent" above. Under a Jelly host a preview's
|
|
2114
|
+
`reach.set.preview`, `request.answer.preview` and `contact.request.preview` are under "Consent" above. Under a Jelly host a preview's
|
|
1912
2115
|
grant names its action's class, `local` (§8.11).
|
|
1913
2116
|
- **Failed delivery of a task request:**
|
|
1914
2117
|
- a `rejected` receipt or nack moves the requester's record to `TASK_STATE_REJECTED`;
|
|
@@ -1929,8 +2132,7 @@ delegation chain is verified after these (§6.7). A person refuses another perso
|
|
|
1929
2132
|
- Receipts, `task.status` and `task.artifact` answer the recipient's own traffic, so they have a
|
|
1930
2133
|
budget of their own: four times that.
|
|
1931
2134
|
- Presence has a budget of its own, also four times that.
|
|
1932
|
-
-
|
|
1933
|
-
`LIMITS.inboundPerMachinePerMinute`, apart from agent mail.
|
|
2135
|
+
- Person-addressed controls (contact requests, cards and grants) have their own budget of `LIMITS.inboundPerMachinePerMinute`.
|
|
1934
2136
|
- Over the limit, an envelope is **deferred**: the recipient withholds its `recv.ack` and does not
|
|
1935
2137
|
mark it seen, so the relay sends it again later (§7 "Delivery") and it is taken when the budget
|
|
1936
2138
|
has room. A backlog that arrives at once is so taken at the budget's pace, all of it, not
|
|
@@ -1939,9 +2141,7 @@ delegation chain is verified after these (§6.7). A person refuses another perso
|
|
|
1939
2141
|
can wait.
|
|
1940
2142
|
- An envelope over the limit that would expire before the budget has room for it, with a minute
|
|
1941
2143
|
to spare for the relay's next redelivery, can never be taken. It is dropped (acked). A message
|
|
1942
|
-
sender gets at most one `rate_limited` receipt a minute.
|
|
1943
|
-
with `rate_limited` for every such chat envelope, up to four times the chat budget a minute, so
|
|
1944
|
-
its line is `refused` at once instead of `sent` until it expires.
|
|
2144
|
+
sender gets at most one `rate_limited` receipt a minute. Person-addressed controls get individual refusals, bounded by the response budget.
|
|
1945
2145
|
- Presence over its budget is dropped: it is never queued, and the next one supersedes it.
|
|
1946
2146
|
- **Quota.** A recipient may cap the inbox items and bytes it holds from one sender machine
|
|
1947
2147
|
(`rate_limited` past it).
|
|
@@ -1954,6 +2154,26 @@ delegation chain is verified after these (§6.7). A person refuses another perso
|
|
|
1954
2154
|
- **`wake` on an inbox item.** This is the sender's hint, AND-ed with the session's tier, which must be
|
|
1955
2155
|
`push`. Only what consent delivers becomes an inbox item, so a held request never wakes a session.
|
|
1956
2156
|
|
|
2157
|
+
- **Mixed versions** (0.7.1 and the conversations release). Neither side shims the other, and neither leaves a
|
|
2158
|
+
half-open contact:
|
|
2159
|
+
- A daemon from before conversations sends a chat line, plain mail or a message request without a portable proof
|
|
2160
|
+
as an envelope. A current daemon refuses each with a `rejected` receipt, code `refused`, and the `note` "The
|
|
2161
|
+
recipient runs a newer Messaging that takes conversations only: update Messaging to reach them." 0.7.1 records
|
|
2162
|
+
only the code: it shows `refused` with its hint "do not retry; tell your person", the clearest it can show, since
|
|
2163
|
+
none of its hints names an update. The note is for later senders and the logs.
|
|
2164
|
+
- A current daemon asks the relay (`conv.caps`, §7) before it creates a dm, group or channel with a person, adds
|
|
2165
|
+
one, or sends a message request. A person whose known devices all lack the `conversations` presence feature is
|
|
2166
|
+
refused with `peer_outdated`, "<handle> needs to update Messaging", and nothing is created or sent. With no
|
|
2167
|
+
presence for that person in the envelope TTL, the relay has no evidence, and the call goes ahead.
|
|
2168
|
+
A request that then reaches a client before conversations is filed there and answered `delivered`, which a
|
|
2169
|
+
current recipient never sends for a request (it holds it, and an accept activates the dm at the relay). The
|
|
2170
|
+
sender's daemon takes a `delivered` or `read` receipt for a request envelope from that recipient's device as that
|
|
2171
|
+
proof: `request.waiting` shows the request with state `outdated` (the recipient must update Messaging, and the
|
|
2172
|
+
request must then be sent again), instead of `held` forever.
|
|
2173
|
+
- Tasks keep their envelopes and work in both directions.
|
|
2174
|
+
- Rollout: the relay first (it serves 0.7.1 unchanged), then every daemon of a realm together
|
|
2175
|
+
(`docs/distribution.md`, "Versions and compatibility").
|
|
2176
|
+
|
|
1957
2177
|
### 8.4 Delivery stages (sender view)
|
|
1958
2178
|
|
|
1959
2179
|
- The normal sequence is `accepted → relayed → delivered → handed_to_agent → read`. A request the
|
|
@@ -2036,8 +2256,8 @@ then lives in plaintext in the sender's and recipient's stores for their retenti
|
|
|
2036
2256
|
and in the recipient's model context. The sending daemon therefore refuses such content before it
|
|
2037
2257
|
stores or seals anything. The relay never sees plaintext, so this check exists only on the sender.
|
|
2038
2258
|
|
|
2039
|
-
- **Where.** `
|
|
2040
|
-
and artifact), `task.cancel` (`reason`)
|
|
2259
|
+
- **Where.** `task.create`, `task.continue`, `conv.send`, `conv.edit`, `task.update` (status message
|
|
2260
|
+
and artifact), and `task.cancel` (`reason`). The daemon reads every string in
|
|
2041
2261
|
the call's params except the session `key`: values and object keys at any depth, including
|
|
2042
2262
|
`metadata` and `data` parts, and the base64-decoded bytes of every `raw` field, read as UTF-8.
|
|
2043
2263
|
- **When.** After the key and task-role checks (`not_found`, `invalid_state` for a wrong role), and
|
|
@@ -2076,26 +2296,34 @@ stores or seals anything. The relay never sees plaintext, so this check exists o
|
|
|
2076
2296
|
|
|
2077
2297
|
### 8.8 Store schema
|
|
2078
2298
|
|
|
2079
|
-
The daemon's store (`messaging.db`) and the relay's database each have one baseline, schema 1
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
- **
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2299
|
+
The daemon's store (`messaging.db`) and the relay's database each have one baseline, schema 1, as release 0.7.0
|
|
2300
|
+
created it (owner, 2026-11-08):
|
|
2301
|
+
|
|
2302
|
+
- **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
|
|
2303
|
+
person uses (Jelly's hosted users), so no later release resets a store or raises a generation.
|
|
2304
|
+
- **From 0.7.0 on, stores only grow**, so that a host can roll a daemon or relay back by moving the binary back
|
|
2305
|
+
and keeping the data. A store change adds an idempotent migration and a new schema number, under the rules of
|
|
2306
|
+
"When stores grow" below; the baseline is never edited.
|
|
2307
|
+
- The daemon store is at **schema 4** and the relay database at **schema 3**, each the baseline plus its migrations:
|
|
2308
|
+
- daemon schema 2 adds `conv_logs`, `conv_entries`, `conv_shares`, `conv_cursors`, `conv_outbox`, `conv_keys`,
|
|
2309
|
+
`conv_alarms` and `conv_fts`; schema 3 adds `conv_held`; schema 4 adds `conv_evidence`;
|
|
2310
|
+
- relay schema 2 adds `conv_logs`, `conv_entries`, `conv_members` and `conv_shares` (§7 "Conversation frames");
|
|
2311
|
+
schema 3 adds `conv_request_decisions`.
|
|
2312
|
+
Each keeps `min_reader` 1, so a 0.7.x daemon or relay still opens the store after a rollback and ignores the new
|
|
2313
|
+
tables.
|
|
2087
2314
|
|
|
2088
2315
|
**Records.** The daemon's store keeps `generation`, `schema` and `min_reader`, the lowest schema that can read
|
|
2089
2316
|
it, in its `kv` table. The relay keeps `generation` and `min_reader` in its `meta` table, and `schema` in
|
|
2090
|
-
`PRAGMA user_version`.
|
|
2091
|
-
0.6.0 have no generation (generation 1).
|
|
2317
|
+
`PRAGMA user_version`. Every store from 0.7.0 on records generation 2 and `min_reader` 1; a new daemon store records
|
|
2318
|
+
schema 4, and a new relay database schema 3. The stores of 0.1.0 to 0.6.0 have no generation (generation 1).
|
|
2092
2319
|
|
|
2093
|
-
**Opening.** A store with no tables is new: the daemon or relay writes the baseline and
|
|
2320
|
+
**Opening.** A store with no tables is new: the daemon or relay writes the baseline, its records and every migration. For the relay,
|
|
2094
2321
|
the tables Litestream adds when it replicates the database (`_litestream_*`) do not count. It opens
|
|
2095
2322
|
any other store only when the store records its own generation and a `min_reader` at most its own schema. It
|
|
2096
|
-
refuses every other store and changes nothing in it
|
|
2097
|
-
relay, move the home (or the relay database,
|
|
2098
|
-
|
|
2323
|
+
refuses every other store and changes nothing in it. The error gives the reason and its remedy (below).
|
|
2324
|
+
Only `pre-reset` calls for starting over: stop the daemon or relay, move the home (or the relay database,
|
|
2325
|
+
with its `-wal` and `-shm` files) aside, and set it up again. For `newer`, upgrade the reader and keep the data.
|
|
2326
|
+
A daemon checks the store before it reads or writes anything else in its home. Nothing deletes a home or
|
|
2099
2327
|
database by itself.
|
|
2100
2328
|
|
|
2101
2329
|
**A refused store** is machine-readable, so that a client never matches the wording. Every refusal has one of two
|
|
@@ -2124,6 +2352,9 @@ database by itself.
|
|
|
2124
2352
|
client that denies its agents the home also denies `<home>.pre-reset-*`: the moved home keeps the old keys and
|
|
2125
2353
|
messages. The messagingd that wrote it still opens it.
|
|
2126
2354
|
|
|
2355
|
+
**Daemon schema 5** adds `reach_grants` and `reach_reviews`: the person's relation grants (§6.9, "Reach choices") and
|
|
2356
|
+
their review entries, apart from `grants`. An older daemon ignores them, and so does not enforce the choices (§6.9).
|
|
2357
|
+
|
|
2127
2358
|
**When stores grow.**
|
|
2128
2359
|
|
|
2129
2360
|
- A daemon or relay opens any store of its generation whose `min_reader` is at most its own schema. It
|
|
@@ -2137,10 +2368,13 @@ database by itself.
|
|
|
2137
2368
|
- A breaking change needs a new store file with an explicit export and import, never an in-place change.
|
|
2138
2369
|
`min_reader` is raised only in an emergency, and hosts that pin the daemon are told first.
|
|
2139
2370
|
|
|
2140
|
-
**The test.** `store-schemas
|
|
2141
|
-
(tables, columns, indexes, constraints, triggers)
|
|
2142
|
-
|
|
2143
|
-
|
|
2371
|
+
**The test.** `store-schemas/<n>.json` in `packages/daemon` and `packages/relay` is the shape of schema n as
|
|
2372
|
+
released (tables, columns, indexes, constraints, triggers); `1.json` is the baseline as 0.7.0 created it.
|
|
2373
|
+
`test/release/store-schema.test.ts` in each package holds the baseline to `1.json` and HEAD's new store to the last
|
|
2374
|
+
snapshot, so a store change is seen in review. Each also opens a database the schema-1 release itself wrote
|
|
2375
|
+
(`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
|
|
2376
|
+
changes nothing, and that a schema-1 reader still opens the result: its records allow it, and every table, column
|
|
2377
|
+
and index of schema 1 is unchanged inside it.
|
|
2144
2378
|
|
|
2145
2379
|
The wire rule (§14) is separate; a rolled-back daemon must still speak to newer peers within `messaging/1`.
|
|
2146
2380
|
|
|
@@ -2247,19 +2481,21 @@ and names each session an engine may register.
|
|
|
2247
2481
|
parent writable by no one else and owned by the daemon's user or root.
|
|
2248
2482
|
- `--socket-group <gid>` makes both sockets 0660 and owned by that group, and `run/` (and the home, when
|
|
2249
2483
|
`run/` is inside it) 0710 to it. It is accepted only here: elsewhere `run/admin.sock` is always 0600.
|
|
2250
|
-
- **Action grants.** On `run/admin.sock`, every method outside `AGENT_METHODS
|
|
2251
|
-
compact JWS in the request's top-level `grant`
|
|
2484
|
+
- **Action grants.** On `run/admin.sock`, every method outside `AGENT_METHODS`, and every call whose params carry an
|
|
2485
|
+
action `token` (§8.2, "Action tokens"), needs an action grant, a compact JWS in the request's top-level `grant`
|
|
2486
|
+
member, beside `params`:
|
|
2252
2487
|
- the header is `{alg: "EdDSA", typ: "jelly-act+jwt", kid}`, and the signature verifies with the action
|
|
2253
2488
|
key (whose `kid`, when the key file has one, the header must name);
|
|
2254
2489
|
- `aud` is `"@joe-broadhead/opencode-messaging#service"`;
|
|
2255
2490
|
- `act` is the method, and `in` is `base64url(SHA-256(JCS(params)))` of the params as sent (absent
|
|
2256
|
-
params are `{}`)
|
|
2491
|
+
params are `{}`). The params include any action `token` (§8.2), so the host's grant signer sees the token. That
|
|
2492
|
+
is expected: the host's client received it in the view;
|
|
2257
2493
|
- `exp - iat` is in (0, 60] seconds, `iat` at most 5 s ahead of the daemon's clock and `exp` at most 5 s
|
|
2258
2494
|
behind it;
|
|
2259
2495
|
- `cls` is required, and is the method's action class (`ACTION_CLASSES`, `actionClass` in `@messaging/core`):
|
|
2260
|
-
`chat` for `
|
|
2261
|
-
read-only human-tier methods and `device.list` (`conv.list`, `conv.read`, `conv.
|
|
2262
|
-
`people.get`, `grant.list`, `request.list`, `
|
|
2496
|
+
`chat` for `conv.send`, `conv.edit`, `conv.delete`, `conv.react` and `notice.ack`; `view` for the reads a host's section makes, the
|
|
2497
|
+
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`,
|
|
2498
|
+
`people.get`, `grant.list`, `reach.get`, `request.list`, `sessions.list`, `tasks.list`, `tasks.get`, `notice.subscribe`, `notice.list`,
|
|
2263
2499
|
`device.list`); `local` for every other method, the previews and every other admin-tier method included.
|
|
2264
2500
|
(`device.status` is agent tier: it needs no grant.) So `view` is refused on any method that writes, and a preview, which tells what
|
|
2265
2501
|
a `local` action would do, takes `local`. A plugin's `jelly` declaration must give each action and read the same
|
|
@@ -2273,8 +2509,8 @@ and names each session an engine may register.
|
|
|
2273
2509
|
`person.attached`, `daemon.snapshot`, `wake.arm` and `wake.disarm`.
|
|
2274
2510
|
It is bound to the first connection that presents it, and may be presented again on that connection until
|
|
2275
2511
|
it expires; on another connection, or after a restart, it is `grant_invalid`. Under a host-scope grant
|
|
2276
|
-
`conv.list`
|
|
2277
|
-
|
|
2512
|
+
`conv.list` is refused before content is read. Lane 4 exposes no placeholder counts.
|
|
2513
|
+
A `notice.subscribe` under a host-scope grant ends when the grant does: the
|
|
2278
2514
|
host subscribes again with a fresh grant and `after`.
|
|
2279
2515
|
- **Refusals.** No grant is `grant_required`; any failure above is `grant_invalid`. The agent tier needs no
|
|
2280
2516
|
grant on either socket. Both are checked before the params are read. The daemon logs each refusal with
|
|
@@ -2309,7 +2545,16 @@ and names each session an engine may register.
|
|
|
2309
2545
|
request envelope ids of those agents' requester tasks that have not ended (submitted, working, input- or
|
|
2310
2546
|
auth-required), most recently updated first, at most 500. Their updates name the request in `replyTo` (§6.2), so the
|
|
2311
2547
|
relay can match them without reading a body. Mail an agent sent and a reply it may get is not awaited: only a task
|
|
2312
|
-
is.
|
|
2548
|
+
is. The daemon also adds `dms`: the verified dm conversations of which this person is a member whose other
|
|
2549
|
+
participant's person shares a `channel` with this person (both seated there as persons, not only through an
|
|
2550
|
+
agent, and `member`, in that channel's verified log) and passes `shouldArm` (not blocked or muted by this
|
|
2551
|
+
person's grants, and not hidden by their reach choices: under Never a person of another org with no chat on their
|
|
2552
|
+
person grant, §6.9), most recently active first, at most `WAKE_DMS`. The daemon reads the candidates' directory
|
|
2553
|
+
views in batches before it decides. It is the host's own list: no other member learns it, and the daemon logs only
|
|
2554
|
+
its count. The list is what held when the host armed: a person blocked, muted or hidden afterwards (on another of
|
|
2555
|
+
this person's devices, while the host sleeps) stays armed until the next `wake.arm`. That is at most one wake: the
|
|
2556
|
+
ticket is single-use, and the wake budget applies. It returns `{relay: "armed" | "pending", awaiting, dms}` (the counts): `pending` while the relay is not
|
|
2557
|
+
connected, and the arm then
|
|
2313
2558
|
goes out at the next welcome (a newer `wake.arm` or `wake.disarm` replaces it; one the relay refuses is not sent
|
|
2314
2559
|
again). `wake.disarm {}` forgets it, and `daemon.woke` disarms a ticket armed since the last disarm. The daemon never
|
|
2315
2560
|
logs the ticket.
|
|
@@ -2481,6 +2726,9 @@ implementation:
|
|
|
2481
2726
|
`in` digest, a method grant, a host-scope grant and a session ticket signed by it, and grants a daemon must
|
|
2482
2727
|
refuse (another method, params, key, `kid`, `typ` or audience; a lifetime past 60 or 600 s; an expired grant;
|
|
2483
2728
|
a `cls` of another class; host scope for a method outside its list).
|
|
2729
|
+
- `action-tokens.json` (§8.2, "Action tokens"): a token, held requests with their actions, the `request.answer` and
|
|
2730
|
+
`task.cancel` params that redeem one, and shapes to refuse (an action that is not an answer, a short or padded
|
|
2731
|
+
token, a token with `all`, a cancel with neither or both of `key` and `token`).
|
|
2484
2732
|
|
|
2485
2733
|
`packages/core/scripts/vectors.ts` generates them. Run `npm run build && npm run vectors -w @messaging/core`;
|
|
2486
2734
|
this uses Node's type stripping against the built package and `@messaging/core/testing`. `packages/core/test/vectors.test.ts`
|
|
@@ -2500,30 +2748,38 @@ daemon owned by the uid in `MESSAGING_DAEMON_UID` (§8.1).
|
|
|
2500
2748
|
|
|
2501
2749
|
### Agent tools
|
|
2502
2750
|
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
`inbox`, `read`, `search`, `tasks`, `task_update` and `task_cancel`.
|
|
2751
|
+
**Conversation tools.** `ConversationTools` is the shared schema registry for the conversation-model
|
|
2752
|
+
cut-over. Its ten verbs are `contacts`,
|
|
2753
|
+
`conversations`, `send`, `inbox`, `read`, `search`, `context`, `tasks`, `task_update` and `task_cancel`.
|
|
2754
|
+
`conversations` uses `conv.access`; `send`, `read`, `search` and `context` use their `conv.*` counterparts.
|
|
2755
|
+
Results identify a line by the stable citation `messaging:<cid>/<id>`. The client fences peer text and names,
|
|
2756
|
+
neutralizes fence closers, and labels own text as ours. List results contain at most 50 events and 32 KiB of text;
|
|
2757
|
+
each fenced event also has the existing per-part cap, `LIMITS.toolTextChars` (32,768).
|
|
2758
|
+
Long bodies have `[truncated: N bytes; read the item for the full text]`, where N counts omitted UTF-8 bytes.
|
|
2759
|
+
The stored body stays intact. `read({source, offset})` continues at the returned `nextOffset` until the full body is shown.
|
|
2760
|
+
A held result waits for consent and does not disclose the person's answer. Tools grant no human-tier action.
|
|
2761
|
+
|
|
2762
|
+
The OpenCode plugin and `messaging-mcp` use the same ten-tool contract in `@messaging/core`, normative like `schema/**`.
|
|
2763
|
+
Their names are
|
|
2506
2764
|
`messaging_<verb>`; in OpenCode's Code Mode, `tools.messaging.<verb>`. No tool grants, adds
|
|
2507
2765
|
contacts, changes devices or administers a team or channel.
|
|
2508
2766
|
|
|
2509
|
-
- **Addresses.** `
|
|
2510
|
-
|
|
2511
|
-
agent
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
`
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
first, with opaque `before`/`after` cursors. Mail and task entries carry previews; outgoing mail
|
|
2522
|
-
carries no text, because the daemon keeps no copy of what it sent.
|
|
2767
|
+
- **Addresses.** Lines accept a canonical `cid` or a person or agent destination.
|
|
2768
|
+
`#<channel>` resolves through accessible conversations. An agent sends to a conversation it is seated in, or to another
|
|
2769
|
+
agent (`person/agent`). With no dm yet, its person's device signs the create of the agent dm, whose only participants
|
|
2770
|
+
are the two agents; the recipient's `ask` admits the invited agent (`open` at once, `request` after its person
|
|
2771
|
+
accepts, `no` never, silently). Until the seat the line waits on the sending device, keeps the id the send returned,
|
|
2772
|
+
shows in `request.waiting` as `pending-delivery`, and lapses after 7 days. An agent cannot start a dm with a person,
|
|
2773
|
+
and cannot add or seat anyone. The sender-side consent is the client's first-send confirmation for the address (the
|
|
2774
|
+
plugin's `messaging.send` permission), as for 0.7.1 mail; the daemon has no separate signal.
|
|
2775
|
+
Retained tasks use `person/agent[@label]`. Malformed addresses are `invalid_request`.
|
|
2776
|
+
An ambiguous task address requires a device label. Human contact requests remain human-only.
|
|
2777
|
+
- **Conversations.** `read({cid})` pages current verified events with opaque cursors.
|
|
2778
|
+
Conversation identity is stable across devices. Legacy room names are not aliases.
|
|
2523
2779
|
- **`contacts`** lists `me` (address, person, agent, device label, relay `connected` or `offline`), the
|
|
2524
|
-
persons in the directory and in
|
|
2525
|
-
`contacts.list` (§8.2), repo, branch and doing), and
|
|
2526
|
-
|
|
2780
|
+
persons in the directory and in conversations, each with their agents (state, `expect` from
|
|
2781
|
+
`contacts.list` (§8.2), repo, branch and doing), and channels allowed by `conv.access`.
|
|
2782
|
+
Counts and read preferences are omitted when unavailable. A person's `can` is the daemon's `reach` for them (§8.2
|
|
2527
2783
|
`contacts.list`): `source: "card"` with `card`, when their person signed it, or `source: "default"`, the
|
|
2528
2784
|
defaults for the org or for a stranger; `can.chat` is false for one's own person and when the client offers no
|
|
2529
2785
|
DMs. An agent's `can` is its person's, except that `can.chat` is false and the inbound policy narrows it (§8.3: a
|
|
@@ -2533,30 +2789,28 @@ contacts, changes devices or administers a team or channel.
|
|
|
2533
2789
|
refused. `can: "ask"` lists those whose `can.ask` is not `no`. A channel's `can` is `default`. `presence` is
|
|
2534
2790
|
`online`,
|
|
2535
2791
|
`offline` or (for a channel) `unknown`. No field is invented.
|
|
2536
|
-
-
|
|
2537
|
-
task
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
fixed `note`. A result at stage `held` (the recipient's person answers a request card first, §6.9)
|
|
2544
|
-
carries `expect: "on_consent"` and a note saying so. `request: {contact: <email>}`, only with `to: "me"`
|
|
2545
|
-
(otherwise `invalid_request`), passes `msg.send {request}`: the line goes to the agent's own person with a
|
|
2546
|
-
suggestion to send a message request to that exact email (§6.9, "Message requests"); the agent cannot send one,
|
|
2547
|
-
and its note says it is not told what the person decides. A send refused `request_required` tells the agent to
|
|
2548
|
-
ask its person this way.
|
|
2792
|
+
- **Sources.** An item target or result names `source: {kind: "conv", cid, event}` or `source: {kind: "task", taskId}`.
|
|
2793
|
+
A task never receives a fabricated conversation id. Conversation item identity includes both `cid` and `event`.
|
|
2794
|
+
- **`send`** uses `conv.send` for lines, including agent DMs. `ask` uses `task.create`.
|
|
2795
|
+
A task continuation uses `task.continue`. Task sends retain `files`, `replyTo`, `onBehalfOf` and `key`.
|
|
2796
|
+
Delegation requires an unfinished task that this session executes. `key` or `clientId` makes retries idempotent.
|
|
2797
|
+
A line returns its conversation source and `pending`. A task returns its task source and delivery results.
|
|
2798
|
+
An authenticated hold carries `expect: "on_consent"`. Missing receipts do not imply a hold.
|
|
2549
2799
|
- **`inbox`** lists mail and task requests (`inbox.list`), unread by default, and acks
|
|
2550
2800
|
`handed_to_agent` for the previews it shows. `consent` says how a request from the sender runs
|
|
2551
2801
|
here: `self` (the session's own person), `trusted` or `guarded`. It is the inbox item's `consent`, the
|
|
2552
2802
|
daemon's decision (§6.9); a client decides it only for a task record or an item stored before
|
|
2553
2803
|
consent, which carry none.
|
|
2554
|
-
- **`read({
|
|
2555
|
-
|
|
2556
|
-
- **`search`**
|
|
2557
|
-
searched.
|
|
2804
|
+
- **`read({source})`** reads the named conversation event or retained task. It preserves the daemon's consent decision.
|
|
2805
|
+
A held event read acknowledges nothing. `read({cid})` returns a conversation page with opaque cursors.
|
|
2806
|
+
- **`search`** uses `conv.search` within the daemon's access rules.
|
|
2558
2807
|
- **`tasks`** lists `task.list` with roles `mine` (requester) and `asked` (executor) and short state
|
|
2559
2808
|
names (`working`, `input_required`, …); `state` also takes `open` and `done`.
|
|
2809
|
+
A task in `auth_required` shows `signIn {tool?, by}`, and one in `rejected` shows `refusal {reason}`, from the task
|
|
2810
|
+
view (§8.2). The tool and the reason are the executor's words, with the author `statusAuthor` names: fenced when the
|
|
2811
|
+
peer wrote them, cleaned and shown without a fence when this side did. `by` is the daemon's, never the peer's. The
|
|
2812
|
+
agent tier never sees `question`, and no tool sets it (the runtime calls `task.question`). `task_update` takes no sign-in tool yet: a plugin that knows which tool is waiting
|
|
2813
|
+
passes it with `task.update {signIn}`.
|
|
2560
2814
|
- **`task_update`** (executor) and **`task_cancel`** (requester) map to `task.update` and
|
|
2561
2815
|
`task.cancel`; a cancel of a finished task returns its state and sends nothing.
|
|
2562
2816
|
- **Errors.** A tool error is `{error: {code, message, remedy}}`: `code` from §13, a `message` that
|
|
@@ -2593,12 +2847,12 @@ contacts, changes devices or administers a team or channel.
|
|
|
2593
2847
|
|
|
2594
2848
|
### Clients
|
|
2595
2849
|
|
|
2596
|
-
- **OpenCode plugin.** Tools under namespace `messaging`: `contacts`, `send`, `inbox`, `read`,
|
|
2597
|
-
`search`, `tasks`, `task_update` and `task_cancel`. Every send asserts `messaging.send` with the
|
|
2850
|
+
- **OpenCode plugin.** Tools under namespace `messaging`: `contacts`, `conversations`, `send`, `inbox`, `read`,
|
|
2851
|
+
`search`, `context`, `tasks`, `task_update` and `task_cancel`. Every send asserts `messaging.send` with the
|
|
2598
2852
|
address as its resource (`person/agent`, `#channel`, or `person` and `person/`), and every read of
|
|
2599
2853
|
a conversation asserts `messaging.read` with the conversation (a `person:` one asks first by
|
|
2600
2854
|
default). Inbound content is framed as untrusted data. Details are in the plugin's repository, `joe-broadhead/opencode-messaging` (`docs/`).
|
|
2601
|
-
- It never
|
|
2855
|
+
- It never changes human read state.
|
|
2602
2856
|
- With wakes on, it registers each session with `host: {kind: "opencode", location, sessionID,
|
|
2603
2857
|
pid}`: its location directory, the OpenCode session id, and the server's pid.
|
|
2604
2858
|
- Its RPC `messaging.opencode/wake {sessionID, envelopeIds}` serves the wake bridge (§8.6). It checks
|
|
@@ -2622,16 +2876,10 @@ contacts, changes devices or administers a team or channel.
|
|
|
2622
2876
|
item only after the host recorded the tool result with the note intact; until then the item is
|
|
2623
2877
|
only offered, and an offer that fails or is cut goes back to waiting. No wire change.
|
|
2624
2878
|
- **MCP server.** `messaging-mcp` (stdio) for other agents. It registers one `pull`-tier session and
|
|
2625
|
-
subscribes to its events.
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
- MCP has no per-conversation approval of its own, so chats are opt-in: the user names channels
|
|
2630
|
-
when configuring the server, with `--channel <name>` (repeatable). `send`, `read` and `search`
|
|
2631
|
-
reach those channels only, and are refused (`forbidden`) for any other channel before any daemon
|
|
2632
|
-
call. Conversations with persons (DM lines) are never available over MCP: a line to a person, or
|
|
2633
|
-
`read`/`search` with one, is `forbidden`. It never calls `conv.subscribe`, `conv.markRead`,
|
|
2634
|
-
`person.send` or `channel.*`.
|
|
2879
|
+
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`.
|
|
2880
|
+
Lines and conversation reads use `conv.*`. Retained tasks use `task.create/continue/list/get/update/cancel` and `inbox.list/get/ack`.
|
|
2881
|
+
- The daemon controls access through `conv.access`. Contacts expose only accessible channel cards.
|
|
2882
|
+
MCP uses the agent socket and never calls human-tier methods or changes human read state.
|
|
2635
2883
|
- It has no peer-turn guard: its agent reads every request itself, with its own tools. It reports
|
|
2636
2884
|
the daemon's `consent` for each item; where the daemon gives none (a task record), `self` for the
|
|
2637
2885
|
session's own person and `trusted` for everyone else.
|
|
@@ -2768,7 +3016,7 @@ contacts, changes devices or administers a team or channel.
|
|
|
2768
3016
|
`prompt` when a dialog on every daemon start is acceptable.
|
|
2769
3017
|
- **Local IPC.** The sockets trust users, not processes (§8.2).
|
|
2770
3018
|
- The daemon enforces two sockets: `run/agent.sock` serves only `AGENT_METHODS`, so never a line as
|
|
2771
|
-
the person
|
|
3019
|
+
the person; `run/admin.sock` serves everything. Agent conversation calls require a registered session.
|
|
2772
3020
|
- Under one OS user that is no boundary: any process of the user can open `run/admin.sock`, call
|
|
2773
3021
|
`team.*` and `daemon.stop`, and act for any session whose key it knows: session keys are bearer
|
|
2774
3022
|
credentials. The tiers keep those methods off agent tool paths, nothing more.
|
|
@@ -2813,6 +3061,10 @@ contacts, changes devices or administers a team or channel.
|
|
|
2813
3061
|
|
|
2814
3062
|
## 13. Error codes
|
|
2815
3063
|
|
|
3064
|
+
Tool errors use the codes in this section for both conversation and task sources (§11).
|
|
3065
|
+
An unknown source is `not_found`. A source that combines task and conversation identifiers is `invalid_request`.
|
|
3066
|
+
Oversized model-visible text is truncated with the explicit marker in §11, rather than reported as `too_large`.
|
|
3067
|
+
|
|
2816
3068
|
| Code | Meaning |
|
|
2817
3069
|
|---|---|
|
|
2818
3070
|
| invalid_request | malformed input or schema violation |
|
|
@@ -2825,6 +3077,7 @@ contacts, changes devices or administers a team or channel.
|
|
|
2825
3077
|
| refused | recipient policy refuses this sender |
|
|
2826
3078
|
| declined | the recipient's person declined a held request (§6.9) |
|
|
2827
3079
|
| request_required | the recipient's person has not accepted this sender: another org's person needs a message request first (§6.9) |
|
|
3080
|
+
| 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 |
|
|
2828
3081
|
| rate_limited | too many envelopes; retry later |
|
|
2829
3082
|
| loop_guard | agent-to-agent loop guard tripped |
|
|
2830
3083
|
| too_large | size limit exceeded |
|
|
@@ -2838,11 +3091,12 @@ contacts, changes devices or administers a team or channel.
|
|
|
2838
3091
|
| 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) |
|
|
2839
3092
|
| roster_conflict | roster entry does not extend the current head |
|
|
2840
3093
|
| invite_invalid | invite unknown, expired, revoked, used or mismatched |
|
|
2841
|
-
| no_identity | the daemon has
|
|
3094
|
+
| no_identity | the daemon has no usable identity: no team, or a directory device not yet bound with published keys (§15.9) |
|
|
2842
3095
|
| binding_invalid | a device binding, a device-log entry or its authority failed verification (§15) |
|
|
2843
|
-
| jelly_unavailable | Jelly ID
|
|
3096
|
+
| 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` |
|
|
2844
3097
|
| log_conflict | a device-log entry does not extend the log's head (§15.3); resync and retry |
|
|
2845
|
-
| 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 |
|
|
3098
|
+
| 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. Also a control op (`conv.add`, `conv.remove`, …) on a conversation that kept queuing events through three drains: "the conversation is busy sending; try again", which can be retried as is |
|
|
3099
|
+
| conflict | a person's setting changed since the caller read it: `reach.set` with a stale `revision` (§8.2); nothing changed, read again |
|
|
2846
3100
|
| 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 |
|
|
2847
3101
|
| device_limit | the person already has `LIMITS.devicesPerPerson` current devices (§15.3) |
|
|
2848
3102
|
| admission_required | `dir.bind` of an entry that is not a device binding itself with a `jelly` authority (§15.10) |
|
|
@@ -2852,8 +3106,8 @@ contacts, changes devices or administers a team or channel.
|
|
|
2852
3106
|
| directory_unavailable | the key directory could not be reached to adopt a tree head or verify a person within the freshness bound (§15.9) |
|
|
2853
3107
|
| login_unsupported | Jelly ID's metadata names no device sign-in, or none for this realm's resource (§15.9, "Signing in through the browser") |
|
|
2854
3108
|
| login_denied | the person denied a device sign-in at Jelly ID, or the account is disabled or being deleted (§15.9) |
|
|
2855
|
-
| grant_required | `deployment: "jelly"`: a human- or admin-tier call carried no action grant (§8.11) |
|
|
2856
|
-
| grant_invalid | `deployment: "jelly"`: the action grant did not verify, is for another method, params or audience, is out of its lifetime, was spent, or is a host-scope grant bound to another connection (§8.11) |
|
|
3109
|
+
| grant_required | `deployment: "jelly"`: a human- or admin-tier call carried no action grant (§8.11), or a `request.answer` no action token (§8.2) |
|
|
3110
|
+
| grant_invalid | `deployment: "jelly"`: the action grant did not verify, is for another method, params or audience, is out of its lifetime, was spent, or is a host-scope grant bound to another connection (§8.11). Any deployment: an action token that the daemon did not offer for this action, target and view, or that was spent or has ended (§8.2, "Action tokens") |
|
|
2857
3111
|
| ticket_required | `deployment: "jelly"`: `session.register` carried no session ticket (§8.11) |
|
|
2858
3112
|
| ticket_invalid | `deployment: "jelly"`: the session ticket did not verify, is out of its lifetime, or names another session or agent (§8.11) |
|
|
2859
3113
|
| daemon_unavailable | a client cannot reach the daemon |
|
|
@@ -3360,11 +3614,9 @@ when a restore does not reach it: keep it on other storage than the database, or
|
|
|
3360
3614
|
who accepts the fork on purpose moves the witness aside and starts again.
|
|
3361
3615
|
|
|
3362
3616
|
**Resolution.** From each person's newest verified Jelly token the directory keeps their name, their email when
|
|
3363
|
-
Jelly ID proved it, and from that email their **handle** and **org** (`
|
|
3617
|
+
Jelly ID proved it, and from that email their **handle** and **org** (`hostedHandle`, `emailOrg`):
|
|
3364
3618
|
|
|
3365
|
-
- the handle
|
|
3366
|
-
trimmed from both ends, cut to 30 characters (trimmed again), and `u-` put in front when it does not start with
|
|
3367
|
-
a letter;
|
|
3619
|
+
- the handle uses `hostedHandle`, exactly as §15.8 defines it;
|
|
3368
3620
|
- the org is `domain:<d>` when the email's domain `d` is in the operator's `orgDomains` (`--org-domains`), and
|
|
3369
3621
|
none otherwise: such a person is an individual, in no org.
|
|
3370
3622
|
|
|
@@ -3414,9 +3666,12 @@ anchor must be an `epoch` anchor whose root is the directory's at that size (`di
|
|
|
3414
3666
|
`rate_limited` with `retryAfterMs`. The per-day counts, and the copies of an open request, are kept in memory,
|
|
3415
3667
|
from UTC midnight.
|
|
3416
3668
|
- **Presence** (`to: {team: true}`) goes only to devices that `presence.watch` the person and are in the person's
|
|
3417
|
-
**audience**: the person themselves, the same org,
|
|
3418
|
-
receipt in the last 30 days
|
|
3419
|
-
|
|
3669
|
+
**audience**: the person themselves, the same org, persons who each sent the other an envelope other than a
|
|
3670
|
+
receipt in the last 30 days, or persons who are both accepted members of one conversation the relay sequences
|
|
3671
|
+
(§7 "Conversation frames"), for as long as both are. The relay reads that from the plaintext op state it already
|
|
3672
|
+
holds: an invited participant does not count, a removed or departed one stops counting, and an external dm counts
|
|
3673
|
+
only once activated (its request accepted). A watcher outside the audience learns nothing, not even "offline". The
|
|
3674
|
+
relay sends no `peer` notices.
|
|
3420
3675
|
|
|
3421
3676
|
**Erasure.** Erasing a person appends an `unbind` of each current device, sets their map value to the tombstone
|
|
3422
3677
|
(§15.5) at the next epoch, deletes their profile, every log body, witness and keys statement and every pair they
|
|
@@ -3455,17 +3710,45 @@ five minutes, until it answers, so a new directory becomes ready without waiting
|
|
|
3455
3710
|
`dir_keys`, `dir_epochs`, `dir_map` (each person's committed value), `dir_rotations`, `dir_pairs` (with
|
|
3456
3711
|
`line_at`, when the pair last carried an envelope other than a receipt), `dir_jelly_keys`, `dir_feed`, and
|
|
3457
3712
|
`dir_wakes`: each Jelly host's device's armed wake ticket, its `agents` and `awaiting`, its `exp` and when it may
|
|
3458
|
-
next be presented (§7, "Wake tickets")
|
|
3713
|
+
next be presented (§7, "Wake tickets"), and `dir_wake_dms` (relay schema 4): the dms each armed device named, with
|
|
3714
|
+
the `exp` of the ticket they were armed with, so a ticket armed again without them does not keep them. At start the directory rebuilds the map and the epoch log from them
|
|
3459
3715
|
and refuses to start if they disagree with the last tree head.
|
|
3460
3716
|
|
|
3461
3717
|
## 16. Conversations
|
|
3462
3718
|
|
|
3719
|
+
**Guest acceptance (X3-1).** An invited participant is not a content reader.
|
|
3720
|
+
The relay serves authenticated control history and heads to invitees, with message digests instead of ciphertext and a zero share cursor.
|
|
3721
|
+
It pushes invitation operations, never message content or keys, to a person represented only by invited seats.
|
|
3722
|
+
`ConvLog.checkShare` requires an accepted recipient. For a group or history `none`, acceptance (including seating an invited
|
|
3723
|
+
agent) is the effective addition: it updates `since` and requires an `added` rekey before messages or shares.
|
|
3724
|
+
Inviting alone does not start that epoch. Shared channels keep their declared shared-history exposure after acceptance.
|
|
3725
|
+
A guest cannot invite or change to another role, even via an intermediate member role. Agents inherit their owner's
|
|
3726
|
+
guest restriction; a guest agent needs an accepted, visible guest owner and that owner's signed seat. Live org checks
|
|
3727
|
+
cover agent principals too; two persons without an org are not implicitly one org.
|
|
3728
|
+
|
|
3729
|
+
A guest's `accept` carries `invitation`, a `GuestInvitation` signed under `DOMAIN.guestInvitation`.
|
|
3730
|
+
The preview binds the invitation ID, inviter device, realm, conversation, invitee, exact op head, expiry, current audience and exposure policies.
|
|
3731
|
+
The device that signed the original add or create signs this bounded preview. The inviter seals it to the invitee without history or keys.
|
|
3732
|
+
The accept event signs the same preview. A changed audience, policy or op head requires a fresh preview.
|
|
3733
|
+
After acceptance, the invitee fetches the log and verifies the invitation independently before content or key use.
|
|
3734
|
+
`guestHistoryExposure` supplies explicit text for shared history and history from the acceptance boundary.
|
|
3735
|
+
|
|
3736
|
+
**Agent-DM admission.** The recipient's `ask` policy controls an agent-only DM invitation, including invitations across orgs.
|
|
3737
|
+
`open` seats the recipient agent, `request` holds the invitation for the person, and `no` silently declines it locally.
|
|
3738
|
+
`request.list` exposes a held invitation as `{kind:"agent-invitation",id,cid,from,to,at}`, without content or keys.
|
|
3739
|
+
`request.answer` accepts `accept` or `decline`. Its preview identifies the sender and target agent and reports the affected count.
|
|
3740
|
+
With `all`, the answer applies to held agent invitations from the same person.
|
|
3741
|
+
|
|
3742
|
+
The daemon records invitation-only history gaps durably. After acceptance, it fetches those entries again and verifies each signature against its stored digest.
|
|
3743
|
+
It replaces only matching gaps and applies the conversation's history policy before content use.
|
|
3744
|
+
This recovery survives restart. It never restores messages that local retention expired after acceptance.
|
|
3745
|
+
|
|
3463
3746
|
A conversation is one hash-chained log of signed events that the relay sequences and members verify. Membership,
|
|
3464
3747
|
roles, epochs and settings are a function of the log. Message bodies are sealed under per-device sender keys, so a
|
|
3465
3748
|
post is one ciphertext whatever the number of members. The schemas are in `schema/conversation.ts`, the rules in
|
|
3466
|
-
`conversation/` of core, and the vectors in `docs/spec/vectors/conversation.json`. The relay
|
|
3467
|
-
storage and the IPC surface
|
|
3468
|
-
Until they land, chats are as §6.8 says.
|
|
3749
|
+
`conversation/` of core, and the vectors in `docs/spec/vectors/conversation.json`. The relay carries the logs in
|
|
3750
|
+
the conversation frames of §7. The daemon's storage and the IPC surface are not specified yet;
|
|
3751
|
+
`docs/design/conversations.md` describes them. Until they land, chats are as §6.8 says.
|
|
3469
3752
|
|
|
3470
3753
|
### 16.1 Identifiers
|
|
3471
3754
|
|
|
@@ -3500,19 +3783,23 @@ Every event has a header and a body. The header is:
|
|
|
3500
3783
|
- `opHead` is the last op the author applied: its `seq` and `h(seq)`, or `{0, h(0)}` before any.
|
|
3501
3784
|
- `seen` is the head the author had, for gossip (§16.4).
|
|
3502
3785
|
- `class` is:
|
|
3503
|
-
- `op`: a control op
|
|
3786
|
+
- `op`: a control op (`ConvOp`), because the relay routes and checks authority by it. Every piece of state is an
|
|
3787
|
+
op, and ops never expire (§16.4); an op whose content must stay private carries it sealed (§16.5);
|
|
3504
3788
|
- `msg`: a sealed `MsgBody`, with the `epoch` and the ratchet index `idx` of its key (§16.5). `wake` (at most
|
|
3505
|
-
`LIMITS.wakeHints` (5)) names sessions of `by.person` the event may wake; it is the only plaintext hint
|
|
3506
|
-
|
|
3789
|
+
`LIMITS.wakeHints` (5)) names sessions of `by.person` the event may wake; it is the only plaintext hint. The
|
|
3790
|
+
author's daemon sets it only on a line that addresses the author's own agents, and the relay acts on it only for
|
|
3791
|
+
the authenticated author's person's devices (§7, "Wake tickets");
|
|
3792
|
+
- `keys`: a sender-key share sealed to one device (§16.6). It is signed like every event but is **not an entry
|
|
3793
|
+
of the log**: the relay keeps it for its recipient alone.
|
|
3507
3794
|
- `sig` covers the whole event: `Ed25519(by.device, "messaging/1/conv-event\n" || JCS(event without sig))`.
|
|
3508
3795
|
- An event is at most `LIMITS.convEventBytes` (256 KiB) of JCS (`too_large`). Every object is strict: an unknown
|
|
3509
3796
|
member is `invalid_request`.
|
|
3510
3797
|
|
|
3511
|
-
The relay stores and serves **entries**: `{seq, prev, event}
|
|
3512
|
-
The relay does not sign entries; members verify the chain (§16.4).
|
|
3798
|
+
The relay stores and serves **entries**: `{seq, prev, event}` of ops and msgs, where `seq` counts from 1 and `prev`
|
|
3799
|
+
is `h(seq − 1)`. The relay does not sign entries; members verify the chain (§16.4).
|
|
3513
3800
|
|
|
3514
3801
|
**Ops** (`ConvOp`): `create`, `add`, `accept`, `join`, `leave`, `remove`, `role`, `seat`, `settings`,
|
|
3515
|
-
`archive` and `
|
|
3802
|
+
`archive`, `rekey`, `policy`, `meta` and `setting`.
|
|
3516
3803
|
|
|
3517
3804
|
- `create` states the kind, the visibility (channels), the history policy, `retainDays` (1 to
|
|
3518
3805
|
`LIMITS.retainDaysMax` (365)), the org a public channel is open to, the participants as seats `{p, role,
|
|
@@ -3523,7 +3810,13 @@ The relay does not sign entries; members verify the chain (§16.4).
|
|
|
3523
3810
|
`none`), and accepts its invite. Only an agent's seat carries `device` or `respondTo`.
|
|
3524
3811
|
- `settings` changes the visibility, history, retention or permits. `archive {on}` archives or restores.
|
|
3525
3812
|
- `rekey {epoch, cause}` starts the next epoch; `cause` is `removed`, `device`, `added`, `settings` or
|
|
3526
|
-
`schedule`.
|
|
3813
|
+
`schedule`. A device rekey also names the removed `device` and may give `until`, a chain position strictly before
|
|
3814
|
+
the rekey. Other causes carry neither field.
|
|
3815
|
+
- `policy {agentContext?, bridges?}` sets one or both, in plaintext (§16.8, §16.10). In a dm or a group it is the
|
|
3816
|
+
author's vote.
|
|
3817
|
+
- `meta {epoch, idx, sealed}`: a group's or channel's `{name?, topic?, purpose?}`, sealed (§16.5).
|
|
3818
|
+
- `setting {kind, epoch, idx, sealed}`: one of the person's settings in their `self` conversation, sealed;
|
|
3819
|
+
`kind` is `prefs`, `grant`, `context`, `localRetention` or `link` (§16.3).
|
|
3527
3820
|
|
|
3528
3821
|
### 16.3 Message bodies
|
|
3529
3822
|
|
|
@@ -3539,17 +3832,25 @@ The plaintext of a `msg` event is one `MsgBody`, at most `LIMITS.convPlaintextBy
|
|
|
3539
3832
|
`status` and `artifact` carry the A2A update events (§6.2) and `cancel` and `followUp` the requester's, each
|
|
3540
3833
|
naming the root.
|
|
3541
3834
|
- `receipt {target, stage}`: `delivered`, `handed_to_agent` or `read`.
|
|
3542
|
-
- `meta {name?, topic?, purpose?}` and `policy {agentContext?, bridges?}`, which sets one or both (§16.8, §16.10).
|
|
3543
3835
|
- `sync {sync}`: a body of the person's `self` conversation, and only there.
|
|
3544
3836
|
|
|
3837
|
+
A conversation's name, topic and purpose, and its policy, are not message bodies: they are state, so they are ops
|
|
3838
|
+
(`meta`, `policy`), which never expire.
|
|
3839
|
+
|
|
3545
3840
|
A mention is `{p}` (a participant), `{all: "channel"}` or `{all: "here"}`. An attachment names a blob, its own
|
|
3546
3841
|
AES-256-GCM key and nonce, its SHA-256 and size (at most `LIMITS.blobBytes` (25 MiB)), a media type and a name.
|
|
3547
3842
|
|
|
3548
3843
|
**The self conversation.** A person's `self` conversation has one participant, the person, and history `shared`.
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
`
|
|
3844
|
+
It holds two kinds of state:
|
|
3845
|
+
|
|
3846
|
+
- **Passing state**, in `sync` bodies (`SyncBody`), which expire with the retention: `read {cid, upTo, thread?}`,
|
|
3847
|
+
`unread {cid, from}`, `follow`, `save`, `draft`, `joined` and `keys {cid, shares}` (at most `LIMITS.keyShares`
|
|
3848
|
+
(512) chain keys).
|
|
3849
|
+
- **Settings**, in `setting` ops (`SettingBody`, sealed), which never expire: `prefs {cid, notify, muteUntil?,
|
|
3850
|
+
receipts?}`, `grant` (a consent grant, §6.9, verified over its raw value), `context {rules}` (§16.8),
|
|
3851
|
+
`localRetention {cid, days}` and `link {link}` (§16.10). The op's `kind` is the body's `s`.
|
|
3852
|
+
|
|
3853
|
+
The last writer wins by `seq`.
|
|
3553
3854
|
|
|
3554
3855
|
### 16.4 The hash chain and gossip
|
|
3555
3856
|
|
|
@@ -3560,9 +3861,24 @@ raw value), `context {rules}` (§16.8), `localRetention {cid, days}` and `keys {
|
|
|
3560
3861
|
fetches up to `seen.seq` first), `consistent` when its `h(seen.seq)` is `seen.hash`, `diverged` otherwise. A
|
|
3561
3862
|
divergence proves that the relay showed two members different logs; the signed event is the evidence. `seen`
|
|
3562
3863
|
at or past the event's own `seq` is `invalid_request`.
|
|
3563
|
-
- **
|
|
3564
|
-
|
|
3565
|
-
|
|
3864
|
+
- **Device cuts.** A `rekey` with cause `device` ends the named device's authority in this conversation: later
|
|
3865
|
+
entries by that device are invalid. If `until` is present, known entries by that device after `until` and before
|
|
3866
|
+
the rekey are cut from state. An absent `until` makes no earlier cut. The position need not name that device's
|
|
3867
|
+
entry: an expired message is a digest with no author or state effect. Replay collects the cuts, verifies every
|
|
3868
|
+
chain link, and does not apply cut entries; a cut rekey cannot cut another device. A cut that removes genesis or
|
|
3869
|
+
leaves later entries unauthorized is refused. A live verifier checks that the named device has been removed in
|
|
3870
|
+
its verified realm. A cut never extends a removed device's authority. Several device rekeys may name one device:
|
|
3871
|
+
an entry is cut when any of their cuts covers it. A daemon asked for a cut stricter than a rekey of the same device
|
|
3872
|
+
it has queued or sent sends the stricter one too, ahead of anything else queued, before the call that drains it
|
|
3873
|
+
returns: a queued rekey stands in for a request only when its cut is at least as strict.
|
|
3874
|
+
- **Retention.** Only `msg` entries expire. When the relay prunes a `msg` entry older than the conversation's
|
|
3875
|
+
`retainDays` it keeps the entry's `seq`, `prev` and the digest `SHA-256(JCS(event))` and serves those
|
|
3876
|
+
(`ConvExpiredEntry`), so the chain stays verifiable (`ConvLog.applyExpired`). **Ops never expire**: membership,
|
|
3877
|
+
roles, epochs, settings, policy votes, sealed meta and the `self` conversation's settings are ops, so a member
|
|
3878
|
+
that replays the log after pruning, or a joiner reading it from genesis, computes the same state as a member
|
|
3879
|
+
that kept every body. The create (seq 1) never expires (`invalid_request`), and an op head that names an
|
|
3880
|
+
expired entry is `conv_conflict`, so a relay that withholds an op as expired is caught by the next op. A key
|
|
3881
|
+
share (§16.6) is not an entry; the relay deletes it after the retention.
|
|
3566
3882
|
|
|
3567
3883
|
### 16.5 Sender keys
|
|
3568
3884
|
|
|
@@ -3585,6 +3901,10 @@ raw value), `context {rules}` (§16.8), `localRetention {cid, days}` and `keys {
|
|
|
3585
3901
|
of order it keeps at most `LIMITS.skippedKeys` (1000) derived but unused keys per (epoch, sender device),
|
|
3586
3902
|
dropping the lowest indexes first; a message further than that past the chain is `too_large`, and one whose key
|
|
3587
3903
|
is used or dropped is `not_found`. It keeps each share's chain key at the share's index, to share history (§16.6).
|
|
3904
|
+
- **Sealed ops.** A `meta` or `setting` op seals its content (`ConvMeta`, or a `SettingBody` of the op's `kind`)
|
|
3905
|
+
the same way, under the author device's key at the op's (`epoch`, `idx`), with `aad = JCS(event without sig, the
|
|
3906
|
+
op without sealed)`, so the AAD binds the op's kind and key index too. The plaintext is at most
|
|
3907
|
+
`LIMITS.convPlaintextBytes`. A sealed op takes one index of the author's chain, as a message does.
|
|
3588
3908
|
- **Epochs.** A `rekey` op starts the next epoch. Each sender then starts a new chain and shares it only with the
|
|
3589
3909
|
devices of the participant persons at the rekey's `seq`.
|
|
3590
3910
|
- Authenticity comes from the event signature, not from the key: a member who holds a sender key still cannot
|
|
@@ -3593,9 +3913,9 @@ raw value), `context {rules}` (§16.8), `localRetention {cid, days}` and `keys {
|
|
|
3593
3913
|
|
|
3594
3914
|
### 16.6 Key shares
|
|
3595
3915
|
|
|
3596
|
-
A `keys` event
|
|
3597
|
-
|
|
3598
|
-
|
|
3916
|
+
A `keys` event goes to **one recipient device**. Its body is `{to: {person, device}, epk, nonce, ct}`; a sender
|
|
3917
|
+
sends one event per recipient device. `ct` seals `JCS({shares: [KeyShare]})`, where a `KeyShare` is `{epoch,
|
|
3918
|
+
device, idx, ck}` (at most `LIMITS.keyShares` (512)):
|
|
3599
3919
|
|
|
3600
3920
|
1. Generate an ephemeral X25519 key pair; `epk` is its raw public key, `nonce` 12 random bytes.
|
|
3601
3921
|
2. `shared = X25519(eph_priv, recipient.enc)`, the recipient device's key as the realm verified it (§4, §15.7).
|
|
@@ -3606,6 +3926,23 @@ A `keys` event's body is `{shares: [{to: {person, device}, epk, nonce, ct}]}`, o
|
|
|
3606
3926
|
The AAD binds a share to its event and its recipient: moving it to another event or device makes it fail. A
|
|
3607
3927
|
receiver keeps the first share for a given (epoch, device, idx); a later one with another key changes nothing.
|
|
3608
3928
|
|
|
3929
|
+
**Not an entry.** A `keys` event is not in the hash chain (`ConvLog.apply` refuses one, `invalid_request`). The
|
|
3930
|
+
relay keeps it for its recipient and serves it to no one else (`ConvShareEntry {n, event}`, §7): each device
|
|
3931
|
+
fetches only its own shares, so a rekey costs a device one share per sender, not every share of every sender. A
|
|
3932
|
+
recipient checks a share with `ConvLog.checkShare`, in the state at the share's `opHead`:
|
|
3933
|
+
|
|
3934
|
+
1. I-JSON, size and the schema; the realm and the `cid` (`invalid_request`).
|
|
3935
|
+
2. The signature over the raw event (`bad_signature`).
|
|
3936
|
+
3. Live: `ts` within `LIMITS.clockSkewMs` (`expired`), and `by.device` a current device of `by.person`
|
|
3937
|
+
(`unknown_device`).
|
|
3938
|
+
4. `by.agent` absent (`forbidden`); `opHead` names an op of the log (`conv_conflict`).
|
|
3939
|
+
5. At `opHead`, the author's person is a member participant person (`not_member`) and `to.person` a participant
|
|
3940
|
+
person (`forbidden`); live, `to.device` a current device of `to.person` (`unknown_device`).
|
|
3941
|
+
|
|
3942
|
+
The relay also requires `opHead` to be the current op head (§7), so no share reaches a device whose person was
|
|
3943
|
+
removed since. A relay that withholds a share is not hidden: the recipient cannot open that sender's messages,
|
|
3944
|
+
and shows that it waits for keys.
|
|
3945
|
+
|
|
3609
3946
|
### 16.7 Authority
|
|
3610
3947
|
|
|
3611
3948
|
`ConvLog.apply` checks each entry, in order, against the state after every earlier entry, and a failed check
|
|
@@ -3619,7 +3956,8 @@ changes nothing:
|
|
|
3619
3956
|
5. `id` not already in the log (`duplicate`); `seen` before the event (`invalid_request`).
|
|
3620
3957
|
6. The first entry is `create`, and there is only one (`conv_conflict`).
|
|
3621
3958
|
7. For an `op`: `opHead` is the current op (`conv_conflict`); `by.agent` is absent: an agent never writes an op
|
|
3622
|
-
(`forbidden`). For a `msg
|
|
3959
|
+
(`forbidden`). For a `msg`: `opHead` names an op of this log, or `{0, h(0)}` (`conv_conflict`). A `keys` event
|
|
3960
|
+
is never an entry (`invalid_request`, §16.6).
|
|
3623
3961
|
8. The rules below. An author who is not a participant is `not_member`, one who lacks the authority `forbidden`, a
|
|
3624
3962
|
target that is not a message of this log `not_found`, and a target in the wrong state `invalid_state`.
|
|
3625
3963
|
|
|
@@ -3639,6 +3977,9 @@ participant's.
|
|
|
3639
3977
|
| `seat` | The target is an agent participant whose person is the author. It becomes `member`. |
|
|
3640
3978
|
| `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`). |
|
|
3641
3979
|
| `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. |
|
|
3980
|
+
| `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. |
|
|
3981
|
+
| `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`). |
|
|
3982
|
+
| `setting` | Only in a `self` conversation; the author is its person. Its `epoch` is the current epoch and no rekey is due (`stale_epoch`). |
|
|
3642
3983
|
| any `msg` | The author's participant is a member; `epoch` is the current epoch and no rekey is due (`stale_epoch`). |
|
|
3643
3984
|
| `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. |
|
|
3644
3985
|
| `edit` | Not archived; the target is a line, not deleted, by the same participant (person and agent). |
|
|
@@ -3647,10 +3988,8 @@ participant's.
|
|
|
3647
3988
|
| `pin` | Not archived; an admin when `pins` is `admins`; the target is not deleted; at most `LIMITS.pins` (100) pinned (`too_large`). |
|
|
3648
3989
|
| `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`). |
|
|
3649
3990
|
| `receipt` | The target is a message of the log. |
|
|
3650
|
-
| `meta` | Not in a dm; in a channel, an admin. |
|
|
3651
|
-
| `policy` | Not by an agent, and not in a `self` conversation. In a channel, an admin; in a dm or group, any participant person, as a vote. |
|
|
3652
3991
|
| `sync` | Only in a `self` conversation, where no other body goes (`invalid_request`). |
|
|
3653
|
-
| `keys` |
|
|
3992
|
+
| `keys` | Not an entry: §16.6 (`ConvLog.checkShare`). |
|
|
3654
3993
|
|
|
3655
3994
|
**State.** A participant starts as `member`, except an agent whose person is not the author (`invited` until its
|
|
3656
3995
|
person seats it) and a `guest` (`invited` until they accept). Live, adding a person of another org as anything but a
|
|
@@ -3658,9 +3997,10 @@ guest is `forbidden`. A rekey is **due** after a removal or a leave (cause `remo
|
|
|
3658
3997
|
and a change of the history policy (`settings`); a channel does not rekey on add. While a rekey is due, every `msg`
|
|
3659
3998
|
is `stale_epoch`: no message is sealed under a key a removed device holds. A `rekey` clears every due cause.
|
|
3660
3999
|
|
|
3661
|
-
`ConvLog` keeps the state after every entry: `at(seq)` is the state of a log holding only entries 1 to `seq`.
|
|
3662
|
-
|
|
3663
|
-
|
|
4000
|
+
`ConvLog` keeps the state after every entry: `at(seq)` is the state of a log holding only entries 1 to `seq`. Only
|
|
4001
|
+
ops change it, so the relay, which cannot open bodies, and a member whose message bodies expired compute the same
|
|
4002
|
+
state. The rules that read a message body (edit, delete, reactions, pins, tasks) run only when the caller passes
|
|
4003
|
+
the opened body.
|
|
3664
4004
|
|
|
3665
4005
|
### 16.8 Agents and history as context
|
|
3666
4006
|
|
|
@@ -3670,7 +4010,8 @@ policy decides whether members' agents may mine it at all.
|
|
|
3670
4010
|
- **Policy** (`AgentContext`): `members` (members' agents may, each under its person's rules), `seats` (only
|
|
3671
4011
|
agents seated in it, by their seats) or `none`. The default is `members`. In a channel an admin sets it with a
|
|
3672
4012
|
`policy` body; in a dm or group each participant person votes, and the strictest vote counts (`none` <
|
|
3673
|
-
`seats` < `members`). A person's vote goes when they leave.
|
|
4013
|
+
`seats` < `members`). A person's vote goes when they leave. The policy is a `policy` op, in plaintext, so a joiner
|
|
4014
|
+
that holds no earlier keys knows it too.
|
|
3674
4015
|
- **Rules** (`ContextRule`): `{agent, scope, access, lookbackDays?}`, where `agent` is a session name or `*`,
|
|
3675
4016
|
`scope` is `{cid}` or `{kind}` with `kind` one of `own`, `dm`, `group`, `channel.private` and
|
|
3676
4017
|
`channel.public`, and `access` is `allow`, `ask` or `off`. A person holds at most `LIMITS.contextRules` (500),
|
|
@@ -3714,7 +4055,7 @@ policy decides whether members' agents may mine it at all.
|
|
|
3714
4055
|
| `LIMITS.retainDaysDefault`, `LIMITS.retainDaysMax` | 90 days, 365 days |
|
|
3715
4056
|
| `LIMITS.skippedKeys`, `LIMITS.chainMessages`, `LIMITS.chainMaxMs` | 1000, 500, 7 days |
|
|
3716
4057
|
| `LIMITS.rekeyScheduleMs` | 30 days |
|
|
3717
|
-
| `LIMITS.keyShares
|
|
4058
|
+
| `LIMITS.keyShares` | 512 chain keys per share |
|
|
3718
4059
|
| `LIMITS.wakeHints`, `LIMITS.contextRules` | 5, 500 |
|
|
3719
4060
|
|
|
3720
4061
|
### 16.10 Linked channels (reserved)
|
|
@@ -3727,8 +4068,8 @@ agents from a phone. The fields below are part of the schema and are validated,
|
|
|
3727
4068
|
signed with the event, so every member sees it. Only a person writes it (`forbidden` on an agent's line, §16.7).
|
|
3728
4069
|
The person's own devices also check it against the person's links (`checkVia`): a link of that channel, hosted by
|
|
3729
4070
|
the authoring device, in force. Other members cannot see the links and do not check that.
|
|
3730
|
-
- **`link`**, a
|
|
3731
|
-
revokedAt?}`.
|
|
4071
|
+
- **`link`**, a setting of the person's `self` conversation (a sealed `setting` op of kind `link`, which never
|
|
4072
|
+
expires): `{id, channel, account, host, proof, authority, linkedAt, revokedAt?}`.
|
|
3732
4073
|
- `account` is the channel's id for the person's account; it is never in the device log, the directory or
|
|
3733
4074
|
presence.
|
|
3734
4075
|
- `host` is the device that runs the bridge.
|
|
@@ -3736,9 +4077,77 @@ agents from a phone. The fields below are part of the schema and are validated,
|
|
|
3736
4077
|
person matched on both sides.
|
|
3737
4078
|
- `authority` is a non-empty subset of `chat` and `view` (§8.11), and never more.
|
|
3738
4079
|
- For each `id` the record written last is the one in force, unless it has `revokedAt` (`currentLinks`).
|
|
3739
|
-
- **`bridges`** in `policy
|
|
4080
|
+
- **`bridges`** in the `policy` op: who may mirror the conversation to a linked channel: `none` (the default), `admins` or
|
|
3740
4081
|
`members`. In a channel an admin sets it. In a dm or group the strictest vote of the participant persons counts,
|
|
3741
4082
|
and a person who has not voted counts as `none`, so bridging needs every participant person's vote.
|
|
3742
4083
|
- **Reader ids** (`ReaderId`): a person, or `ext:<channel>:<link id>` for a link's channel provider. They are not on
|
|
3743
4084
|
the wire; the audience guard's reader sets use them.
|
|
3744
4085
|
- A foreign sender on a bridged channel (`via.sender`) is not part of v1: the schema refuses it.
|
|
4086
|
+
|
|
4087
|
+
### 16.11 Same-realm contact request cut-over
|
|
4088
|
+
|
|
4089
|
+
This profile replaces the first-message import rules of §6.9 for conversation DMs. It does not introduce cross-realm addresses.
|
|
4090
|
+
The normative shapes are in `schema/contact-request.ts`. The request plaintext uses `RequestImport`, separate from the ordinary `MsgBody` parser.
|
|
4091
|
+
The cut-over engine uses `openRequestImport` and `applyRequestEntry` for this profile. Source attribution comes from verified request evidence, never the wrapper author.
|
|
4092
|
+
|
|
4093
|
+
**Portable source proof.** A new request contains a sender-signed descriptor and a sealed opening:
|
|
4094
|
+
|
|
4095
|
+
- The descriptor binds `v`, `realm`, `cid`, `from`, `to`, `id`, `issuedAt`, `expiresAt`, `commitment` and `by`.
|
|
4096
|
+
- The sender signs its raw JCS bytes without `sig`, under `DOMAIN.request` (`messaging/1/contact-request\n`).
|
|
4097
|
+
- The opening contains a fresh random 256-bit `salt` and 1–500 UTF-16 code units of `text`.
|
|
4098
|
+
- The commitment is SHA-256 of `DOMAIN.requestContent` and JCS of the opening.
|
|
4099
|
+
- The descriptor digest is SHA-256 of `DOMAIN.requestDigest` and JCS of the signed descriptor.
|
|
4100
|
+
- Both device signatures require bindings from retained, verified identity history. An absent binding fails closed.
|
|
4101
|
+
|
|
4102
|
+
The request names the deterministic DM of two distinct persons. The consent window cannot exceed `LIMITS.ttlMs`.
|
|
4103
|
+
Every device copy carries the same descriptor and opening. Only sealed envelopes carry the opening before acceptance.
|
|
4104
|
+
Requests expose no agent address, task, mention, attachment, context-index entry or wake hint.
|
|
4105
|
+
The envelope payload has `conv: {kind: "request", proof: PortableRequest}`. Its one text part and message ID match the signed opening and request ID.
|
|
4106
|
+
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.
|
|
4107
|
+
The optional field in the stored payload schema permits reading retained old records. It does not authorize old plaintext for a conversation import.
|
|
4108
|
+
|
|
4109
|
+
**Terminal decision.** The recipient signs `RequestDecision` under `DOMAIN.requestDecision` (`messaging/1/contact-request-decision\n`).
|
|
4110
|
+
The decision binds the descriptor digest, request ID, realm, DM, both persons, answer and decision time.
|
|
4111
|
+
The answer is `accept` or `decline`. A local block sends `decline` and keeps the blocking grant private.
|
|
4112
|
+
The decision time must be within the original consent window. Retries preserve the signed decision and its timestamp.
|
|
4113
|
+
|
|
4114
|
+
`request.settle {req, descriptor, decision}` stores the first valid terminal decision in the recipient mailbox.
|
|
4115
|
+
Only the connected recipient device can submit its own signed decision. A later conflicting decision returns the first decision.
|
|
4116
|
+
`request.state {req, descriptor}` returns that decision to either named person, or no decision before settlement.
|
|
4117
|
+
Both frames answer `request.settled {req, id, decision?}`. They expose neither salt nor text to the relay.
|
|
4118
|
+
Relay schema 3 adds `conv_request_decisions`. Generation stays 2 and the minimum reader stays 1.
|
|
4119
|
+
Relay schema 4 adds `dir_wake_dms` (§7, "Wake tickets"; §15). Generation stays 2 and the minimum reader stays 1.
|
|
4120
|
+
|
|
4121
|
+
**Permanent activation.** An external human DM declares `create.admission: "request"`.
|
|
4122
|
+
The relay refuses an external DM without this declaration. Same-org DMs retain implicit org admission.
|
|
4123
|
+
Before activation, content and key shares fail with `request_required`. The recipient appends `activate {proof: {descriptor, decision}}` after mailbox reconciliation.
|
|
4124
|
+
Device-removal rekeys can precede activation. They still require the normal removed-device and log-authority checks, and grant no content access.
|
|
4125
|
+
The activation is a permanent op. It reserves the request ID for exactly one import.
|
|
4126
|
+
|
|
4127
|
+
The recipient seals `RequestImport {t: "request.import", descriptor, decision, opening}` under its conversation sender key.
|
|
4128
|
+
The wrapper has `id` and `request` equal to the reserved request ID. It has no agent author or wake hint.
|
|
4129
|
+
Members verify both proofs and the opening before they attribute text to the original sender.
|
|
4130
|
+
The recipient then appends `request.complete {request, seq, digest}`. The digest is SHA-256 of JCS of the exact imported event.
|
|
4131
|
+
This permanent completion survives deletion and retention. Ordinary messages wait until all accepted reservations have completed imports.
|
|
4132
|
+
|
|
4133
|
+
Duplicate importers fetch the winning event and verify its exact proofs and opening. A different body at the same ID is a conflict.
|
|
4134
|
+
The source sender need not be online. Crossed requests use one DM but keep separate IDs, answers and imports.
|
|
4135
|
+
The import is immutable. Only its original sender can delete its text. The wrapper author gains no source-author permissions.
|
|
4136
|
+
After retention, members verify the permanent activation and completion without reconstructing text.
|
|
4137
|
+
|
|
4138
|
+
**Existing contacts.** Each person signs a conversation-bound `ContactAuthorization` under `DOMAIN.contactAuthorization`.
|
|
4139
|
+
An activation with `proof.contacts` requires both reciprocal signatures. The daemon signs only under an existing open or muted chat grant.
|
|
4140
|
+
This path needs no new human prompt and imports no request text. A contact card cannot substitute for either signature.
|
|
4141
|
+
|
|
4142
|
+
**Durable local lifecycle.** `ConversationRequests` stores each request and its state in namespaced rows of the existing `kv` table.
|
|
4143
|
+
Queued sends, submitted requests, decisions and imports survive restart. `waitingOnThem()` exposes only explicit submitted requests, not missing receipts.
|
|
4144
|
+
Acceptance opens only chat. A later block prevents local import and key use even after permanent activation.
|
|
4145
|
+
The sender receives the same error for an unknown or undiscoverable exact email.
|
|
4146
|
+
|
|
4147
|
+
Old pending requests retain their local records and expose `resend`. The sender must send a new request with portable evidence.
|
|
4148
|
+
On the sender's side, a request sent before the cut-over that the recipient's device held for its person and nobody
|
|
4149
|
+
answered (its chat envelope still `sent`, held and unexpired) appears in `request.waiting` with state `resend`, until
|
|
4150
|
+
it lapses (`LIMITS.ttlMs` after it was sent) or a new request to that person replaces it. Nothing is sent for it:
|
|
4151
|
+
resending signs a new request, which needs the person's own send. A request still in flight before the cut-over,
|
|
4152
|
+
not yet held, cannot be told from a chat line and is not shown.
|
|
4153
|
+
The cut-over never signs old plaintext for its sender or silently resends it.
|