@joe-broadhead/messaging 0.7.0 → 0.7.1
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 +45 -0
- package/dist/bin/messaging-mcp.js +649 -12
- package/dist/bin/messaging-relay.js +774 -20
- package/dist/bin/messaging.js +748 -15
- package/dist/bin/messagingd.js +785 -18
- package/dist/lib/modules/core/agent-tools.js +23 -4
- package/dist/lib/modules/core/conversation/context.js +80 -0
- package/dist/lib/modules/core/conversation/event.js +100 -0
- package/dist/lib/modules/core/conversation/ids.js +38 -0
- package/dist/lib/modules/core/conversation/link.js +35 -0
- package/dist/lib/modules/core/conversation/log.js +766 -0
- package/dist/lib/modules/core/conversation/ratchet.js +243 -0
- package/dist/lib/modules/core/conversation/seal.js +198 -0
- package/dist/lib/modules/core/index.js +10 -1
- package/dist/lib/modules/core/schema/common.js +56 -0
- package/dist/lib/modules/core/schema/conversation.js +292 -0
- package/dist/lib/modules/core/schema/errors.js +39 -0
- package/dist/lib/modules/core/schema/index.js +1 -0
- package/dist/lib/modules/core/testing.js +1 -0
- package/dist/lib/modules/core/version.js +1 -1
- package/dist/spec/messaging-v1.md +324 -6
- package/dist/types/core/agent-tools.d.ts +20 -2
- package/dist/types/core/conversation/context.d.ts +56 -0
- package/dist/types/core/conversation/event.d.ts +36 -0
- package/dist/types/core/conversation/ids.d.ts +18 -0
- package/dist/types/core/conversation/link.d.ts +16 -0
- package/dist/types/core/conversation/log.d.ts +142 -0
- package/dist/types/core/conversation/ratchet.d.ts +95 -0
- package/dist/types/core/conversation/seal.d.ts +47 -0
- package/dist/types/core/index.d.ts +8 -1
- package/dist/types/core/schema/common.d.ts +56 -0
- package/dist/types/core/schema/conversation.d.ts +1657 -0
- package/dist/types/core/schema/envelope.d.ts +6 -0
- package/dist/types/core/schema/errors.d.ts +38 -0
- package/dist/types/core/schema/index.d.ts +1 -0
- package/dist/types/core/schema/ipc/device.d.ts +6 -0
- package/dist/types/core/schema/ipc/shapes.d.ts +3 -0
- package/dist/types/core/schema/ipc/surfaces.d.ts +6 -0
- package/dist/types/core/schema/ipc.d.ts +15 -0
- package/dist/types/core/schema/relay.d.ts +12 -0
- package/dist/types/core/testing.d.ts +1 -0
- package/dist/types/core/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -16,8 +16,8 @@ other, exchange messages, and delegate A2A tasks, through a relay that only sees
|
|
|
16
16
|
(`urn:messaging:binding:relay:v1`), presence and session addressing, delivery receipts, and the
|
|
17
17
|
extensions `urn:messaging:ext:provenance:v1` and `urn:messaging:ext:presence:v1`.
|
|
18
18
|
|
|
19
|
-
Not in v1: hole-punched peer-to-peer transport, federation between relays, payments
|
|
20
|
-
(
|
|
19
|
+
Not in v1: hole-punched peer-to-peer transport, federation between relays, and payments. Conversations
|
|
20
|
+
(§16) encrypt to many members with sender keys; MLS is left to a later protocol version.
|
|
21
21
|
|
|
22
22
|
## 2. Encodings
|
|
23
23
|
|
|
@@ -54,8 +54,9 @@ Not in v1: hole-punched peer-to-peer transport, federation between relays, payme
|
|
|
54
54
|
`messaging/1/invite\n`, `messaging/1/invite-join\n`, `messaging/1/delegation\n`, `messaging/1/grant\n`,
|
|
55
55
|
`messaging/1/card\n`, and for hosted realms (§15) the person hash prefix
|
|
56
56
|
`messaging/1/person\n`, `messaging/1/dir-read\n`, `messaging/1/device-log\n`, `messaging/1/device-keys\n`,
|
|
57
|
-
`messaging/1/device-link\n`, `messaging/1/epoch\n` and `messaging/1/dir-key\n
|
|
58
|
-
|
|
57
|
+
`messaging/1/device-link\n`, `messaging/1/epoch\n` and `messaging/1/dir-key\n`, and for conversations (§16) the
|
|
58
|
+
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`). A signature or hash for one purpose never verifies for another.
|
|
59
60
|
- **Hash:** SHA-256, base64url.
|
|
60
61
|
|
|
61
62
|
## 3. Identity
|
|
@@ -2093,11 +2094,36 @@ it, in its `kv` table. The relay keeps `generation` and `min_reader` in its `met
|
|
|
2093
2094
|
the tables Litestream adds when it replicates the database (`_litestream_*`) do not count. It opens
|
|
2094
2095
|
any other store only when the store records its own generation and a `min_reader` at most its own schema. It
|
|
2095
2096
|
refuses every other store and changes nothing in it, and the error says how to start over: stop the daemon or
|
|
2096
|
-
relay, move the home (or the relay database, with its `-wal` and `-shm` files) aside, and set it up again
|
|
2097
|
-
(`messaging-relay` logs it as `relay.store_refused {db, reason}` and exits 1). A
|
|
2097
|
+
relay, move the home (or the relay database, with its `-wal` and `-shm` files) aside, and set it up again. A
|
|
2098
2098
|
daemon checks the store before it reads or writes anything else in its home. Nothing deletes a home or a
|
|
2099
2099
|
database by itself.
|
|
2100
2100
|
|
|
2101
|
+
**A refused store** is machine-readable, so that a client never matches the wording. Every refusal has one of two
|
|
2102
|
+
**reasons**, and the remedy depends on it:
|
|
2103
|
+
|
|
2104
|
+
| Reason | The store | Remedy |
|
|
2105
|
+
|---|---|---|
|
|
2106
|
+
| `pre-reset` | records no store generation, or another generation than the reader's | Start over: move the home (or the relay database) aside and set it up again. |
|
|
2107
|
+
| `newer` | its `min_reader` is above the reader's schema: a newer messagingd or relay wrote it | Upgrade the daemon or relay, and keep the home or database as it is. Never move it aside for this reason. |
|
|
2108
|
+
|
|
2109
|
+
- `messagingd` exits with status **78** (`EX_CONFIG` of sysexits(3); `STORE_REFUSED_EXIT` in `@messaging/core`)
|
|
2110
|
+
for either reason, and prints one line on stderr, `messagingd: store_refused: <reason>: <why, and what to do>`.
|
|
2111
|
+
No other failure exits 78.
|
|
2112
|
+
- `messaging-relay` exits 78 and logs one JSON event, `relay.store_refused {db, reason, message}`: `reason` is
|
|
2113
|
+
`pre-reset` or `newer`, and `message` is the text.
|
|
2114
|
+
- A client that starts the daemon and sees it exit 78 reports the error code `store_refused` (§13), with the reason
|
|
2115
|
+
in a field of its own (`StoreRefusedError.reason`; `reason` in `messaging --json` errors and in agent tool
|
|
2116
|
+
errors). It reads the reason from the two tokens after `messagingd:` on the line that start printed
|
|
2117
|
+
(`storeRefusedReason` in `@messaging/core`) and never parses the text after them, which may change between
|
|
2118
|
+
releases. A line without a reason token gives `store_refused` without a reason. `messaging daemon start` does
|
|
2119
|
+
this.
|
|
2120
|
+
- A client offers to start over (moving the home aside) **only for `pre-reset`**. For `newer`, and for a refusal
|
|
2121
|
+
without a reason, it tells the person to upgrade or to read messagingd's line, and moves nothing.
|
|
2122
|
+
- A home moved aside is named `<home>.pre-reset-<UTC timestamp>`, the timestamp as `YYYYMMDDTHHMMSSZ` (for example
|
|
2123
|
+
`~/.messaging.pre-reset-20261108T171500Z`). The name carries no version, so it holds for every reset, and a
|
|
2124
|
+
client that denies its agents the home also denies `<home>.pre-reset-*`: the moved home keeps the old keys and
|
|
2125
|
+
messages. The messagingd that wrote it still opens it.
|
|
2126
|
+
|
|
2101
2127
|
**When stores grow.**
|
|
2102
2128
|
|
|
2103
2129
|
- A daemon or relay opens any store of its generation whose `min_reader` is at most its own schema. It
|
|
@@ -2319,6 +2345,10 @@ callers rely on.
|
|
|
2319
2345
|
It answers from what this machine has verified, synchronously; a caller catches up before it
|
|
2320
2346
|
verifies. `Roster` implements it: `realm` is the team id, `device` is `member`, `anchor()` is
|
|
2321
2347
|
`rosterAnchor`.
|
|
2348
|
+
- Conversations (§16): `checkVia`, `currentLinks`, `signConvEvent`, `verifyConvEvent`, `sealConvMsg`, `openConvMsg`, `sealConvMeta`,
|
|
2349
|
+
`openConvMeta`, `sealKeyShares`, `openKeyShares`, `ConvLog.apply`, `ConvLog.applyExpired`, `ConvLog.relate`
|
|
2350
|
+
and `resolveContext` throw only `MessagingError`. `ConvLog.at(seq)` is the state a log holding only entries 1
|
|
2351
|
+
to `seq` has. `ReceiverChains.messageKey` hands out each message key once.
|
|
2322
2352
|
- `createGrant` narrows its parent and never widens it (`forbidden`); an expired parent is `expired`.
|
|
2323
2353
|
- Consent (§6.9): `signConsentGrant` refuses a grant that breaks a rule (`invalid_request`) or is
|
|
2324
2354
|
over `LIMITS.grantBytes` (`too_large`); `verifyConsentGrant` checks the schema, the rules, the
|
|
@@ -2812,6 +2842,8 @@ contacts, changes devices or administers a team or channel.
|
|
|
2812
2842
|
| binding_invalid | a device binding, a device-log entry or its authority failed verification (§15) |
|
|
2813
2843
|
| jelly_unavailable | Jelly ID's key set or key history could not be fetched (§15.2); never reported as `binding_invalid` |
|
|
2814
2844
|
| 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 |
|
|
2846
|
+
| stale_epoch | a conversation message is sealed under an epoch that is not the current one, or a rekey is due (§16.7); re-seal and retry |
|
|
2815
2847
|
| device_limit | the person already has `LIMITS.devicesPerPerson` current devices (§15.3) |
|
|
2816
2848
|
| admission_required | `dir.bind` of an entry that is not a device binding itself with a `jelly` authority (§15.10) |
|
|
2817
2849
|
| admission_invalid | `dir.bind` whose Jelly token is not live now, or not for this directory's issuer, resource and person (§15.10) |
|
|
@@ -2826,6 +2858,7 @@ contacts, changes devices or administers a team or channel.
|
|
|
2826
2858
|
| ticket_invalid | `deployment: "jelly"`: the session ticket did not verify, is out of its lifetime, or names another session or agent (§8.11) |
|
|
2827
2859
|
| daemon_unavailable | a client cannot reach the daemon |
|
|
2828
2860
|
| daemon_mismatch | the daemon on the socket is another release or IPC protocol than the client, or did not say which (§8.2) |
|
|
2861
|
+
| store_refused | messagingd refused the home's store and changed nothing; it exits 78 (§8.8). Its `reason` field says why: `pre-reset` (no or another store generation: start over by moving the home aside) or `newer` (a `min_reader` above its schema: upgrade, keep the home); absent when messagingd named none. A client code: a client that starts the daemon reports it, never the daemon over IPC |
|
|
2829
2862
|
| internal | unexpected failure (details stay in local logs) |
|
|
2830
2863
|
|
|
2831
2864
|
## 14. Versioning
|
|
@@ -3424,3 +3457,288 @@ five minutes, until it answers, so a new directory becomes ready without waiting
|
|
|
3424
3457
|
`dir_wakes`: each Jelly host's device's armed wake ticket, its `agents` and `awaiting`, its `exp` and when it may
|
|
3425
3458
|
next be presented (§7, "Wake tickets"). At start the directory rebuilds the map and the epoch log from them
|
|
3426
3459
|
and refuses to start if they disagree with the last tree head.
|
|
3460
|
+
|
|
3461
|
+
## 16. Conversations
|
|
3462
|
+
|
|
3463
|
+
A conversation is one hash-chained log of signed events that the relay sequences and members verify. Membership,
|
|
3464
|
+
roles, epochs and settings are a function of the log. Message bodies are sealed under per-device sender keys, so a
|
|
3465
|
+
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 frames, the daemon's
|
|
3467
|
+
storage and the IPC surface that carry them are not specified yet; `docs/design/conversations.md` describes them.
|
|
3468
|
+
Until they land, chats are as §6.8 says.
|
|
3469
|
+
|
|
3470
|
+
### 16.1 Identifiers
|
|
3471
|
+
|
|
3472
|
+
- A **participant** (`ParticipantId`) is a person (§3, §15.1) or one agent of a person: `person/agent`, where
|
|
3473
|
+
`agent` is a session name (§3). Every agent belongs to one person, its **person**.
|
|
3474
|
+
- A conversation id (`ConvId`) is one of:
|
|
3475
|
+
- a **dm**: `"d_" + base32lower(SHA-256("messaging/1/dm\n" || JCS([a, b] sorted)))[0..26]`, where `a` and `b`
|
|
3476
|
+
are its two participants, sorted by UTF-16 code units. Both ends derive the same id. `a` may equal `b`: a
|
|
3477
|
+
person's note to themselves.
|
|
3478
|
+
- a person's **self** conversation: `"s_" + base32lower(SHA-256("messaging/1/self\n" || JCS(person)))[0..26]`.
|
|
3479
|
+
- a **group** or **channel**: `"c_"` and a ULID its creator chooses.
|
|
3480
|
+
|
|
3481
|
+
base32lower is RFC 4648 §6 in lower case without padding, as for `PersonId` (§15.1).
|
|
3482
|
+
- Kinds (`ConvKind`): `dm`, `group`, `channel` and `self`. Only a channel has a visibility (`public` or
|
|
3483
|
+
`private`). The history policy (`none` or `shared`) says whether a late joiner may read earlier messages.
|
|
3484
|
+
- Roles (`ConvRole`): `owner`, `admin`, `member` and `guest`. A participant's state is `member`, or `invited`
|
|
3485
|
+
until it accepts or is seated (§16.7).
|
|
3486
|
+
|
|
3487
|
+
### 16.2 Events and the log
|
|
3488
|
+
|
|
3489
|
+
Every event has a header and a body. The header is:
|
|
3490
|
+
|
|
3491
|
+
```text
|
|
3492
|
+
{v: "messaging/1", realm, cid, id, by: {person, device, agent?}, ts, opHead: {seq, hash}, seen?: {seq, hash},
|
|
3493
|
+
class, epoch?, idx?, wake?}
|
|
3494
|
+
```
|
|
3495
|
+
|
|
3496
|
+
- `id` is a ULID the author chooses. It is unique in the conversation: the log refuses an event whose `id` it
|
|
3497
|
+
already holds (`duplicate`). Other events refer to an event by `id`.
|
|
3498
|
+
- `by` names the signing device, its person and, for an agent's event, the agent. `by.device` must be a usable
|
|
3499
|
+
Ed25519 key (§3).
|
|
3500
|
+
- `opHead` is the last op the author applied: its `seq` and `h(seq)`, or `{0, h(0)}` before any.
|
|
3501
|
+
- `seen` is the head the author had, for gossip (§16.4).
|
|
3502
|
+
- `class` is:
|
|
3503
|
+
- `op`: a control op in plaintext (`ConvOp`), because the relay routes and checks authority by it;
|
|
3504
|
+
- `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
|
+
- `keys`: sender-key shares, each sealed to one device (§16.6).
|
|
3507
|
+
- `sig` covers the whole event: `Ed25519(by.device, "messaging/1/conv-event\n" || JCS(event without sig))`.
|
|
3508
|
+
- An event is at most `LIMITS.convEventBytes` (256 KiB) of JCS (`too_large`). Every object is strict: an unknown
|
|
3509
|
+
member is `invalid_request`.
|
|
3510
|
+
|
|
3511
|
+
The relay stores and serves **entries**: `{seq, prev, event}`, where `seq` counts from 1 and `prev` is `h(seq − 1)`.
|
|
3512
|
+
The relay does not sign entries; members verify the chain (§16.4).
|
|
3513
|
+
|
|
3514
|
+
**Ops** (`ConvOp`): `create`, `add`, `accept`, `join`, `leave`, `remove`, `role`, `seat`, `settings`,
|
|
3515
|
+
`archive` and `rekey`.
|
|
3516
|
+
|
|
3517
|
+
- `create` states the kind, the visibility (channels), the history policy, `retainDays` (1 to
|
|
3518
|
+
`LIMITS.retainDaysMax` (365)), the org a public channel is open to, the participants as seats `{p, role,
|
|
3519
|
+
device?, respondTo?}`, the permits `post`, `invite` and `pins` (`all` or `admins`; absent is `all`), and
|
|
3520
|
+
`sealedMeta` (§16.5).
|
|
3521
|
+
- `add` names 1 to `LIMITS.convOpParticipants` (100) seats; `remove` names as many participants.
|
|
3522
|
+
- `seat` sets an agent participant's `device` (the device that hosts it) and `respondTo` (`self`, `members` or
|
|
3523
|
+
`none`), and accepts its invite. Only an agent's seat carries `device` or `respondTo`.
|
|
3524
|
+
- `settings` changes the visibility, history, retention or permits. `archive {on}` archives or restores.
|
|
3525
|
+
- `rekey {epoch, cause}` starts the next epoch; `cause` is `removed`, `device`, `added`, `settings` or
|
|
3526
|
+
`schedule`.
|
|
3527
|
+
|
|
3528
|
+
### 16.3 Message bodies
|
|
3529
|
+
|
|
3530
|
+
The plaintext of a `msg` event is one `MsgBody`, at most `LIMITS.convPlaintextBytes` (64 KiB) of JCS
|
|
3531
|
+
(`too_large`):
|
|
3532
|
+
|
|
3533
|
+
- `line`: `text` (at most `LIMITS.lineChars` (40,000) UTF-16 code units), `format` (`plain` or `md`), and
|
|
3534
|
+
optionally `thread` (the root's event id), `broadcast`, `mentions` (at most `LIMITS.mentions` (50)), `files`
|
|
3535
|
+
(at most `LIMITS.files` (10) attachments), `parts` (up to 8 A2A data parts), `answers` (the mention it answers)
|
|
3536
|
+
`grants` (at most `LIMITS.agentMentions` (5) delegation grants, §6.7) and `via` (§16.10).
|
|
3537
|
+
- `edit {target, text, mentions?}`, `delete {target}`, `react {target, key, on}` and `pin {target, on}`.
|
|
3538
|
+
- `task {task}`: a task as a thread. `request {to, message, delegation}` is the root, to an agent participant;
|
|
3539
|
+
`status` and `artifact` carry the A2A update events (§6.2) and `cancel` and `followUp` the requester's, each
|
|
3540
|
+
naming the root.
|
|
3541
|
+
- `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
|
+
- `sync {sync}`: a body of the person's `self` conversation, and only there.
|
|
3544
|
+
|
|
3545
|
+
A mention is `{p}` (a participant), `{all: "channel"}` or `{all: "here"}`. An attachment names a blob, its own
|
|
3546
|
+
AES-256-GCM key and nonce, its SHA-256 and size (at most `LIMITS.blobBytes` (25 MiB)), a media type and a name.
|
|
3547
|
+
|
|
3548
|
+
**The self conversation.** A person's `self` conversation has one participant, the person, and history `shared`.
|
|
3549
|
+
Its `sync` bodies (`SyncBody`) are `read {cid, upTo, thread?}`, `unread {cid, from}`, `prefs {cid, notify,
|
|
3550
|
+
muteUntil?, receipts?}`, `follow`, `save`, `draft`, `joined`, `grant` (a consent grant, §6.9, verified over its
|
|
3551
|
+
raw value), `context {rules}` (§16.8), `localRetention {cid, days}` and `keys {cid, shares}` (at most
|
|
3552
|
+
`LIMITS.keyShares` (512) chain keys) and `link {link}` (§16.10). The last writer wins by `seq`.
|
|
3553
|
+
|
|
3554
|
+
### 16.4 The hash chain and gossip
|
|
3555
|
+
|
|
3556
|
+
- `h(0) = SHA-256("messaging/1/conv-genesis\n" || JCS(cid))`.
|
|
3557
|
+
- `h(seq) = SHA-256(h(seq − 1) || SHA-256(JCS(event)))`, over the raw 32-byte digests.
|
|
3558
|
+
- A member checks `prev = h(seq − 1)` and the signature over the raw received event before it reads any field.
|
|
3559
|
+
- **Gossip.** A member compares an event's `seen` with its own log (`ConvLog.relate`): `ahead` past its head (it
|
|
3560
|
+
fetches up to `seen.seq` first), `consistent` when its `h(seen.seq)` is `seen.hash`, `diverged` otherwise. A
|
|
3561
|
+
divergence proves that the relay showed two members different logs; the signed event is the evidence. `seen`
|
|
3562
|
+
at or past the event's own `seq` is `invalid_request`.
|
|
3563
|
+
- **Retention.** When the relay expires a `msg` or `keys` entry it keeps the entry's `seq`, `prev` and the digest
|
|
3564
|
+
`SHA-256(JCS(event))`, so the chain stays verifiable (`ConvLog.applyExpired`). Ops never expire: an op head that
|
|
3565
|
+
names an expired entry is `conv_conflict`, so a relay that withholds an op as expired is caught by the next op.
|
|
3566
|
+
|
|
3567
|
+
### 16.5 Sender keys
|
|
3568
|
+
|
|
3569
|
+
- **Chains.** A sender device keeps one chain per (conversation, epoch). From a chain key `ck_i`:
|
|
3570
|
+
- the message key `mk_i = HKDF-SHA256(ikm = ck_i, salt = empty, info = "messaging/1/conv-msg", 32 bytes)`;
|
|
3571
|
+
- the next chain key `ck_{i+1} = HKDF-SHA256(ikm = ck_i, salt = empty, info = "messaging/1/conv-chain", 32
|
|
3572
|
+
bytes)`.
|
|
3573
|
+
|
|
3574
|
+
The first chain of an epoch starts at index 0 from 32 random bytes.
|
|
3575
|
+
- **Sealing a body.** `body = base64url(nonce || AES-256-GCM(mk_idx, nonce, JCS(MsgBody), aad = JCS(header)))`,
|
|
3576
|
+
with a fresh 12-byte nonce and the 16-byte tag last; the header is the event without `body` and `sig`. The
|
|
3577
|
+
event's `epoch` and `idx` name the key. A message key seals one body only.
|
|
3578
|
+
- **Sealed meta.** `create.sealedMeta` is `JCS({name?, topic?, purpose?})` sealed the same way under the creator
|
|
3579
|
+
device's epoch-0 key `mk_0`, with the create event's header as AAD. The creator's first message uses index 1.
|
|
3580
|
+
- **Rotation.** A sender starts a new chain in the same epoch after `LIMITS.chainMessages` (500) messages on one
|
|
3581
|
+
chain, or `LIMITS.chainMaxMs` (7 days) after starting it. The new chain starts from fresh random bytes at the
|
|
3582
|
+
next index, so `(epoch, device, idx)` names every key: the share with the greatest index at or below `idx`
|
|
3583
|
+
derives it.
|
|
3584
|
+
- **Receiving.** A receiver derives forward only and uses each message key once. To open messages that arrive out
|
|
3585
|
+
of order it keeps at most `LIMITS.skippedKeys` (1000) derived but unused keys per (epoch, sender device),
|
|
3586
|
+
dropping the lowest indexes first; a message further than that past the chain is `too_large`, and one whose key
|
|
3587
|
+
is used or dropped is `not_found`. It keeps each share's chain key at the share's index, to share history (§16.6).
|
|
3588
|
+
- **Epochs.** A `rekey` op starts the next epoch. Each sender then starts a new chain and shares it only with the
|
|
3589
|
+
devices of the participant persons at the rekey's `seq`.
|
|
3590
|
+
- Authenticity comes from the event signature, not from the key: a member who holds a sender key still cannot
|
|
3591
|
+
write as another member. Within an epoch there is no post-compromise security: a copy of a device's state reads
|
|
3592
|
+
every chain it held until the next rotation or epoch.
|
|
3593
|
+
|
|
3594
|
+
### 16.6 Key shares
|
|
3595
|
+
|
|
3596
|
+
A `keys` event's body is `{shares: [{to: {person, device}, epk, nonce, ct}]}`, one entry per recipient device
|
|
3597
|
+
(at most `LIMITS.keySharesPerEvent` (500); a larger share is split over several events). Each `ct` seals
|
|
3598
|
+
`JCS({shares: [KeyShare]})`, where a `KeyShare` is `{epoch, device, idx, ck}` (at most `LIMITS.keyShares` (512)):
|
|
3599
|
+
|
|
3600
|
+
1. Generate an ephemeral X25519 key pair; `epk` is its raw public key, `nonce` 12 random bytes.
|
|
3601
|
+
2. `shared = X25519(eph_priv, recipient.enc)`, the recipient device's key as the realm verified it (§4, §15.7).
|
|
3602
|
+
3. `key = HKDF-SHA256(ikm = shared, salt = epk || recipient.enc, info = "messaging/1/seal", 32 bytes)`, as for
|
|
3603
|
+
an envelope (§5, "Sealing").
|
|
3604
|
+
4. `ct = AES-256-GCM(key, nonce, JCS({shares}), aad = JCS({header, to}))`, with the tag last.
|
|
3605
|
+
|
|
3606
|
+
The AAD binds a share to its event and its recipient: moving it to another event or device makes it fail. A
|
|
3607
|
+
receiver keeps the first share for a given (epoch, device, idx); a later one with another key changes nothing.
|
|
3608
|
+
|
|
3609
|
+
### 16.7 Authority
|
|
3610
|
+
|
|
3611
|
+
`ConvLog.apply` checks each entry, in order, against the state after every earlier entry, and a failed check
|
|
3612
|
+
changes nothing:
|
|
3613
|
+
|
|
3614
|
+
1. I-JSON (§2, `invalid_request`) and size (`too_large`); the schema, the realm and the `cid` (`invalid_request`).
|
|
3615
|
+
2. The chain: `seq = head + 1` and `prev = h(head)` (`conv_conflict`).
|
|
3616
|
+
3. The signature over the raw event (`bad_signature`).
|
|
3617
|
+
4. When appending live (the relay, or a member checking a pushed event): `ts` within `LIMITS.clockSkewMs` of now
|
|
3618
|
+
(`expired`), and `by.device` a current device of `by.person` in the realm (`unknown_device`).
|
|
3619
|
+
5. `id` not already in the log (`duplicate`); `seen` before the event (`invalid_request`).
|
|
3620
|
+
6. The first entry is `create`, and there is only one (`conv_conflict`).
|
|
3621
|
+
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` or `keys`: `opHead` names an op of this log, or `{0, h(0)}` (`conv_conflict`).
|
|
3623
|
+
8. The rules below. An author who is not a participant is `not_member`, one who lacks the authority `forbidden`, a
|
|
3624
|
+
target that is not a message of this log `not_found`, and a target in the wrong state `invalid_state`.
|
|
3625
|
+
|
|
3626
|
+
A participant **person** is the person of any participant, so the devices of a person whose agent sits in a
|
|
3627
|
+
conversation append its keys and rekeys. "Admin" below means `owner` or `admin`; an author's role is its own
|
|
3628
|
+
participant's.
|
|
3629
|
+
|
|
3630
|
+
| Event | Allowed when |
|
|
3631
|
+
|---|---|
|
|
3632
|
+
| `create` | A **dm**: exactly two seats, both `member`, no visibility, org, permits or meta, history `none`, `cid` the derived one, and the author is the person of one seat. A **self**: one seat, the author as `owner`, `cid` the author's, history `shared`. A **group**: 3 to `LIMITS.groupParticipants` (50) distinct seats, no visibility or org. A **channel**: a visibility, and an org if public, at most `LIMITS.convParticipants` (1000) seats. In a group or channel the author is a seat as `owner`. |
|
|
3633
|
+
| `add` | A group or channel (never a dm or self). The author is a member, not a guest, and an admin when `invite` is `admins`. No seat is a current participant; only an owner adds an owner or an admin; the kind's limit holds (`too_large`). |
|
|
3634
|
+
| `accept` | The author's own participant is `invited`. |
|
|
3635
|
+
| `join` | A public channel the author is not in; live, the author's person is in the channel's org. |
|
|
3636
|
+
| `leave` | A group or channel; the author is a participant. Their agents leave with them. The last owner cannot leave while another person remains. |
|
|
3637
|
+
| `remove` | A group or channel. The author is an admin, and an admin never removes an owner; or the target is the author's own agent (whether or not the author is a participant). Removing a person removes their agents. The last owner cannot be removed. |
|
|
3638
|
+
| `role` | A group or channel. The author is an owner, or an admin setting `member` or `guest` on a participant who is not an owner. The last owner cannot be demoted. |
|
|
3639
|
+
| `seat` | The target is an agent participant whose person is the author. It becomes `member`. |
|
|
3640
|
+
| `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
|
+
| `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. |
|
|
3642
|
+
| any `msg` | The author's participant is a member; `epoch` is the current epoch and no rekey is due (`stale_epoch`). |
|
|
3643
|
+
| `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
|
+
| `edit` | Not archived; the target is a line, not deleted, by the same participant (person and agent). |
|
|
3645
|
+
| `delete` | The target is a line or a task event, not deleted, and the author wrote it or is an admin. |
|
|
3646
|
+
| `react` | As `line`; the target is not deleted; at most `LIMITS.reactionsPerEvent` (50) keys per target (`too_large`). |
|
|
3647
|
+
| `pin` | Not archived; an admin when `pins` is `admins`; the target is not deleted; at most `LIMITS.pins` (100) pinned (`too_large`). |
|
|
3648
|
+
| `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
|
+
| `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
|
+
| `sync` | Only in a `self` conversation, where no other body goes (`invalid_request`). |
|
|
3653
|
+
| `keys` | The author's person is a member participant person and `by.agent` is absent. Every share goes to a participant person (`forbidden`); live, to a current device of that person (`unknown_device`). |
|
|
3654
|
+
|
|
3655
|
+
**State.** A participant starts as `member`, except an agent whose person is not the author (`invited` until its
|
|
3656
|
+
person seats it) and a `guest` (`invited` until they accept). Live, adding a person of another org as anything but a
|
|
3657
|
+
guest is `forbidden`. A rekey is **due** after a removal or a leave (cause `removed`), an add to a group (`added`),
|
|
3658
|
+
and a change of the history policy (`settings`); a channel does not rekey on add. While a rekey is due, every `msg`
|
|
3659
|
+
is `stale_epoch`: no message is sealed under a key a removed device holds. A `rekey` clears every due cause.
|
|
3660
|
+
|
|
3661
|
+
`ConvLog` keeps the state after every entry: `at(seq)` is the state of a log holding only entries 1 to `seq`. Ops
|
|
3662
|
+
and `policy` change it; the rules that read a message body (edit, delete, reactions, pins, tasks) run only when the
|
|
3663
|
+
caller passes the opened body, so the relay, which cannot open bodies, applies the header rules alone.
|
|
3664
|
+
|
|
3665
|
+
### 16.8 Agents and history as context
|
|
3666
|
+
|
|
3667
|
+
A person decides which of their agents may mine which of their conversations as context, and a conversation's
|
|
3668
|
+
policy decides whether members' agents may mine it at all.
|
|
3669
|
+
|
|
3670
|
+
- **Policy** (`AgentContext`): `members` (members' agents may, each under its person's rules), `seats` (only
|
|
3671
|
+
agents seated in it, by their seats) or `none`. The default is `members`. In a channel an admin sets it with a
|
|
3672
|
+
`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.
|
|
3674
|
+
- **Rules** (`ContextRule`): `{agent, scope, access, lookbackDays?}`, where `agent` is a session name or `*`,
|
|
3675
|
+
`scope` is `{cid}` or `{kind}` with `kind` one of `own`, `dm`, `group`, `channel.private` and
|
|
3676
|
+
`channel.public`, and `access` is `allow`, `ask` or `off`. A person holds at most `LIMITS.contextRules` (500),
|
|
3677
|
+
one per (agent, scope), synced in their `self` conversation. A conversation is `own` when its participant persons
|
|
3678
|
+
are the person alone.
|
|
3679
|
+
- **Resolution** (`resolveContext`) for agent A of person P reading conversation C as context:
|
|
3680
|
+
1. a `self` conversation is never mined;
|
|
3681
|
+
2. C's policy `seats` or `none` forbids the read, whatever P's rules say;
|
|
3682
|
+
3. else the most specific rule of P applies (a `cid` scope over a `kind` scope, then A by name over `*`), or
|
|
3683
|
+
with none, the default for C's scope:
|
|
3684
|
+
|
|
3685
|
+
| Scope | Access | Lookback |
|
|
3686
|
+
|---|---|---|
|
|
3687
|
+
| `own` | `allow` | the retention |
|
|
3688
|
+
| `channel.public` | `allow` | 90 days |
|
|
3689
|
+
| `channel.private` | `ask` | 30 days |
|
|
3690
|
+
| `group` | `ask` | 30 days |
|
|
3691
|
+
| `dm` | `ask` | 14 days |
|
|
3692
|
+
|
|
3693
|
+
`off` forbids, `ask` holds the read for P to answer, `allow` serves it;
|
|
3694
|
+
4. the lookback is the smallest of the rule's (or default's) lookback, C's `retainDays` and P's local
|
|
3695
|
+
retention for C.
|
|
3696
|
+
- An agent seated in C reads C from its seat by its seat, not by P's rules, unless C's policy is `none`; then it
|
|
3697
|
+
reads only what is addressed to it and the threads that is in (`seatReads`).
|
|
3698
|
+
- Agents of every client follow these rules, MCP clients included; each daemon enforces them for its own person's
|
|
3699
|
+
agents.
|
|
3700
|
+
|
|
3701
|
+
### 16.9 Limits
|
|
3702
|
+
|
|
3703
|
+
| Limit | Value |
|
|
3704
|
+
|---|---|
|
|
3705
|
+
| `LIMITS.convParticipants` | 1000 participants (persons and agents) in a channel |
|
|
3706
|
+
| `LIMITS.groupParticipants` | 50 in a group |
|
|
3707
|
+
| `LIMITS.convOpParticipants` | 100 per `add` or `remove` |
|
|
3708
|
+
| `LIMITS.convEventBytes` | 256 KiB per event |
|
|
3709
|
+
| `LIMITS.convPlaintextBytes` | 64 KiB per message body |
|
|
3710
|
+
| `LIMITS.lineChars` | 40,000 UTF-16 code units |
|
|
3711
|
+
| `LIMITS.mentions`, `LIMITS.files`, `LIMITS.agentMentions` | 50, 10 and 5 per line |
|
|
3712
|
+
| `LIMITS.blobBytes` | 25 MiB per file |
|
|
3713
|
+
| `LIMITS.reactionsPerEvent`, `LIMITS.pins` | 50 keys per target, 100 pins per conversation |
|
|
3714
|
+
| `LIMITS.retainDaysDefault`, `LIMITS.retainDaysMax` | 90 days, 365 days |
|
|
3715
|
+
| `LIMITS.skippedKeys`, `LIMITS.chainMessages`, `LIMITS.chainMaxMs` | 1000, 500, 7 days |
|
|
3716
|
+
| `LIMITS.rekeyScheduleMs` | 30 days |
|
|
3717
|
+
| `LIMITS.keyShares`, `LIMITS.keySharesPerEvent` | 512 chain keys per share, 500 devices per `keys` event |
|
|
3718
|
+
| `LIMITS.wakeHints`, `LIMITS.contextRules` | 5, 500 |
|
|
3719
|
+
|
|
3720
|
+
### 16.10 Linked channels (reserved)
|
|
3721
|
+
|
|
3722
|
+
A person may link an outside chat account (Telegram, then Slack) as an alias of themselves, to reach their own
|
|
3723
|
+
agents from a phone. The fields below are part of the schema and are validated, but no daemon acts on them yet;
|
|
3724
|
+
`docs/design/conversations.md` §17 describes the plan.
|
|
3725
|
+
|
|
3726
|
+
- **`via {channel}`** on a `line`: the person wrote it from a linked channel, `telegram` or `slack`. It is sealed and
|
|
3727
|
+
signed with the event, so every member sees it. Only a person writes it (`forbidden` on an agent's line, §16.7).
|
|
3728
|
+
The person's own devices also check it against the person's links (`checkVia`): a link of that channel, hosted by
|
|
3729
|
+
the authoring device, in force. Other members cannot see the links and do not check that.
|
|
3730
|
+
- **`link`**, a body of the person's `self` conversation: `{id, channel, account, host, proof, authority, linkedAt,
|
|
3731
|
+
revokedAt?}`.
|
|
3732
|
+
- `account` is the channel's id for the person's account; it is never in the device log, the directory or
|
|
3733
|
+
presence.
|
|
3734
|
+
- `host` is the device that runs the bridge.
|
|
3735
|
+
- `proof` is `{method: "code-sas", codeDigest, sas}`: the digest of the one-time code and the six-digit code the
|
|
3736
|
+
person matched on both sides.
|
|
3737
|
+
- `authority` is a non-empty subset of `chat` and `view` (§8.11), and never more.
|
|
3738
|
+
- For each `id` the record written last is the one in force, unless it has `revokedAt` (`currentLinks`).
|
|
3739
|
+
- **`bridges`** in `policy`: who may mirror the conversation to a linked channel: `none` (the default), `admins` or
|
|
3740
|
+
`members`. In a channel an admin sets it. In a dm or group the strictest vote of the participant persons counts,
|
|
3741
|
+
and a person who has not voted counts as `none`, so bridging needs every participant person's vote.
|
|
3742
|
+
- **Reader ids** (`ReaderId`): a person, or `ext:<channel>:<link id>` for a link's channel provider. They are not on
|
|
3743
|
+
the wire; the audience guard's reader sets use them.
|
|
3744
|
+
- A foreign sender on a bridged channel (`via.sender`) is not part of v1: the schema refuses it.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { TaskState as A2ATaskState } from "./schema/a2a.js";
|
|
3
|
-
import { ErrorCode } from "./schema/errors.js";
|
|
3
|
+
import { ErrorCode, StoreRefusedReason } from "./schema/errors.js";
|
|
4
4
|
/**
|
|
5
5
|
* The agent tool contract (spec §11, "Agent tools"): the eight tools every agent-facing client
|
|
6
6
|
* offers, with one input and one output schema each. The OpenCode plugin registers them as
|
|
@@ -81,6 +81,11 @@ export declare const DONE_STATES: readonly TaskStateName[];
|
|
|
81
81
|
* starts with the code, and `remedy` is "Next: " and this sentence.
|
|
82
82
|
*/
|
|
83
83
|
export declare const TOOL_REMEDIES: Readonly<Record<ErrorCode, string>>;
|
|
84
|
+
/**
|
|
85
|
+
* `store_refused`'s remedy by its reason (spec §8.8), which replaces `TOOL_REMEDIES.store_refused` when the reason is
|
|
86
|
+
* known: only `pre-reset` moves the home aside; `newer` upgrades messagingd and keeps the home.
|
|
87
|
+
*/
|
|
88
|
+
export declare const STORE_REFUSED_REMEDIES: Readonly<Record<StoreRefusedReason, string>>;
|
|
84
89
|
export declare const ToolErrorBody: z.ZodObject<{
|
|
85
90
|
error: z.ZodObject<{
|
|
86
91
|
code: z.ZodEnum<{
|
|
@@ -111,6 +116,8 @@ export declare const ToolErrorBody: z.ZodObject<{
|
|
|
111
116
|
binding_invalid: "binding_invalid";
|
|
112
117
|
jelly_unavailable: "jelly_unavailable";
|
|
113
118
|
log_conflict: "log_conflict";
|
|
119
|
+
conv_conflict: "conv_conflict";
|
|
120
|
+
stale_epoch: "stale_epoch";
|
|
114
121
|
device_limit: "device_limit";
|
|
115
122
|
admission_required: "admission_required";
|
|
116
123
|
admission_invalid: "admission_invalid";
|
|
@@ -125,10 +132,15 @@ export declare const ToolErrorBody: z.ZodObject<{
|
|
|
125
132
|
ticket_invalid: "ticket_invalid";
|
|
126
133
|
daemon_unavailable: "daemon_unavailable";
|
|
127
134
|
daemon_mismatch: "daemon_mismatch";
|
|
135
|
+
store_refused: "store_refused";
|
|
128
136
|
internal: "internal";
|
|
129
137
|
}>;
|
|
130
138
|
message: z.ZodString;
|
|
131
139
|
remedy: z.ZodString;
|
|
140
|
+
reason: z.ZodOptional<z.ZodEnum<{
|
|
141
|
+
"pre-reset": "pre-reset";
|
|
142
|
+
newer: "newer";
|
|
143
|
+
}>>;
|
|
132
144
|
}, z.core.$strip>;
|
|
133
145
|
}, z.core.$strip>;
|
|
134
146
|
export type ToolErrorBody = z.infer<typeof ToolErrorBody>;
|
|
@@ -136,7 +148,7 @@ export type ToolErrorBody = z.infer<typeof ToolErrorBody>;
|
|
|
136
148
|
* The body of a tool error in Jelly's house style (§13.14): `message` starts with the code, `remedy` is
|
|
137
149
|
* the code's one next step. `message` must never hold peer text, bodies or keys.
|
|
138
150
|
*/
|
|
139
|
-
export declare function toolErrorBody(code: ErrorCode, message: string): ToolErrorBody;
|
|
151
|
+
export declare function toolErrorBody(code: ErrorCode, message: string, reason?: StoreRefusedReason): ToolErrorBody;
|
|
140
152
|
export declare const ToolFiles: z.ZodArray<z.ZodObject<{
|
|
141
153
|
name: z.ZodString;
|
|
142
154
|
mime: z.ZodString;
|
|
@@ -465,6 +477,8 @@ export declare const AgentTool: {
|
|
|
465
477
|
binding_invalid: "binding_invalid";
|
|
466
478
|
jelly_unavailable: "jelly_unavailable";
|
|
467
479
|
log_conflict: "log_conflict";
|
|
480
|
+
conv_conflict: "conv_conflict";
|
|
481
|
+
stale_epoch: "stale_epoch";
|
|
468
482
|
device_limit: "device_limit";
|
|
469
483
|
admission_required: "admission_required";
|
|
470
484
|
admission_invalid: "admission_invalid";
|
|
@@ -479,6 +493,7 @@ export declare const AgentTool: {
|
|
|
479
493
|
ticket_invalid: "ticket_invalid";
|
|
480
494
|
daemon_unavailable: "daemon_unavailable";
|
|
481
495
|
daemon_mismatch: "daemon_mismatch";
|
|
496
|
+
store_refused: "store_refused";
|
|
482
497
|
internal: "internal";
|
|
483
498
|
}>>;
|
|
484
499
|
expect: z.ZodOptional<z.ZodEnum<{
|
|
@@ -727,6 +742,8 @@ export declare const AgentTool: {
|
|
|
727
742
|
binding_invalid: "binding_invalid";
|
|
728
743
|
jelly_unavailable: "jelly_unavailable";
|
|
729
744
|
log_conflict: "log_conflict";
|
|
745
|
+
conv_conflict: "conv_conflict";
|
|
746
|
+
stale_epoch: "stale_epoch";
|
|
730
747
|
device_limit: "device_limit";
|
|
731
748
|
admission_required: "admission_required";
|
|
732
749
|
admission_invalid: "admission_invalid";
|
|
@@ -741,6 +758,7 @@ export declare const AgentTool: {
|
|
|
741
758
|
ticket_invalid: "ticket_invalid";
|
|
742
759
|
daemon_unavailable: "daemon_unavailable";
|
|
743
760
|
daemon_mismatch: "daemon_mismatch";
|
|
761
|
+
store_refused: "store_refused";
|
|
744
762
|
internal: "internal";
|
|
745
763
|
}>>;
|
|
746
764
|
}, z.core.$strip>>>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { PersonId, SessionName } from "../schema/common.js";
|
|
2
|
+
import { type AgentContext, type ContextAccess, type ContextKind, type ContextRule, type ConvId, type ConvKind, type Visibility } from "../schema/conversation.js";
|
|
3
|
+
/** The defaults that apply where a person has no rule (decided 2026-11-08). `lookbackDays` absent: the retention. */
|
|
4
|
+
export declare const CONTEXT_DEFAULTS: Readonly<Record<ContextKind, {
|
|
5
|
+
access: ContextAccess;
|
|
6
|
+
lookbackDays?: number;
|
|
7
|
+
}>>;
|
|
8
|
+
/** The facts about a conversation that resolution reads. `persons` are its participant persons at the read. */
|
|
9
|
+
export interface ContextConv {
|
|
10
|
+
cid: ConvId;
|
|
11
|
+
kind: ConvKind;
|
|
12
|
+
visibility?: Visibility;
|
|
13
|
+
persons: readonly PersonId[];
|
|
14
|
+
retainDays: number;
|
|
15
|
+
agentContext: AgentContext;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The scope a conversation falls in for `person`: `own` when its participant persons are `person` alone, else its
|
|
19
|
+
* kind (a channel by its visibility). A `self` conversation has none: no agent mines it.
|
|
20
|
+
*/
|
|
21
|
+
export declare function contextKindOf(person: PersonId, conv: Pick<ContextConv, "kind" | "visibility" | "persons">): ContextKind | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* The person's rule for (agent, conversation), most specific first: a `cid` scope over a `kind` scope, then a named
|
|
24
|
+
* agent over `*`. Undefined when no rule matches; the defaults then apply.
|
|
25
|
+
*/
|
|
26
|
+
export declare function matchContextRule(rules: readonly ContextRule[], agent: SessionName, cid: ConvId, kind: ContextKind): ContextRule | undefined;
|
|
27
|
+
/** The outcome of a context read: served, held for the person (`ask`), or refused. */
|
|
28
|
+
export type ContextDecision = {
|
|
29
|
+
access: "allow" | "ask";
|
|
30
|
+
lookbackDays: number;
|
|
31
|
+
source: "rule" | "default";
|
|
32
|
+
} | {
|
|
33
|
+
access: "forbidden";
|
|
34
|
+
source: "policy" | "rule" | "self";
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The effective rule for agent `agent` of `person` reading `conv` as context (not by its seat):
|
|
38
|
+
* - the conversation's policy `seats` or `none` forbids it, whatever the person's rule says;
|
|
39
|
+
* - else the person's rule, or the default: `off` forbids, `ask` holds, `allow` serves;
|
|
40
|
+
* - the lookback is the smallest of the rule's (or the default's) `lookbackDays`, the conversation's `retainDays`,
|
|
41
|
+
* and the person's local retention for it.
|
|
42
|
+
* Throws `invalid_request` for a rule set that fails `ContextRules`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveContext(args: {
|
|
45
|
+
person: PersonId;
|
|
46
|
+
agent: SessionName;
|
|
47
|
+
conv: ContextConv;
|
|
48
|
+
rules: readonly ContextRule[];
|
|
49
|
+
localRetentionDays?: number;
|
|
50
|
+
}): ContextDecision;
|
|
51
|
+
/**
|
|
52
|
+
* What a seated agent reads by its seat (spec §16.8): everything from its seat's `seq` on, unless the policy is
|
|
53
|
+
* `none`, when it reads only the events addressed to it and the threads they are in. The person's rules do not
|
|
54
|
+
* apply to seat reads.
|
|
55
|
+
*/
|
|
56
|
+
export declare function seatReads(agentContext: AgentContext): "conversation" | "items";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Identity } from "../identity.js";
|
|
2
|
+
import { type Hash } from "../schema/common.js";
|
|
3
|
+
import { type ConvEvent, type ConvEventHeader, type ConvId, type UnsignedConvEvent } from "../schema/conversation.js";
|
|
4
|
+
/** h(0) of `cid`'s log: SHA-256(DOMAIN.convGenesis ‖ JCS(cid)), base64url. */
|
|
5
|
+
export declare function convGenesisHash(cid: ConvId): Hash;
|
|
6
|
+
/** SHA-256(JCS(event)), the raw digest, as the chain links it. Throws `invalid_request` for non-I-JSON. */
|
|
7
|
+
export declare function convEventDigest(event: unknown): Uint8Array;
|
|
8
|
+
/** h(seq) = SHA-256(h(seq − 1) ‖ SHA-256(JCS(event))), over the raw 32-byte digests, base64url. */
|
|
9
|
+
export declare function convEntryHash(prev: Hash, event: unknown): Hash;
|
|
10
|
+
/** `convEntryHash` from the event's digest: what a member checks for an entry whose content has expired. */
|
|
11
|
+
export declare function convLinkHash(prev: Hash, digest: Uint8Array): Hash;
|
|
12
|
+
/** The header of an event: everything but `body` and `sig`. JCS of it is the AAD of a sealed body. */
|
|
13
|
+
export declare function convEventHeader(event: ConvEvent | Omit<ConvEvent, "sig"> | ConvEventHeader): ConvEventHeader;
|
|
14
|
+
/**
|
|
15
|
+
* Checks that `raw` is I-JSON within `LIMITS.jsonDepth` and at most `LIMITS.convEventBytes` (`too_large`), that it
|
|
16
|
+
* matches `ConvEvent`, and that `by.device` is a usable Ed25519 key. Returns a frozen JSON copy of the raw value, so
|
|
17
|
+
* the signature and the hash cover exactly what was received. The signature is not checked.
|
|
18
|
+
*/
|
|
19
|
+
export declare function admitConvEvent(raw: unknown): ConvEvent;
|
|
20
|
+
/**
|
|
21
|
+
* Signs an event (spec §16.4): `sig = Ed25519(by.device, DOMAIN.convEvent ‖ JCS(event without sig))`. `by.device`
|
|
22
|
+
* must be `identity`'s machine. Refuses an event that fails the schema (`invalid_request`) or is over
|
|
23
|
+
* `LIMITS.convEventBytes` once signed (`too_large`). Authority is checked when the event is applied.
|
|
24
|
+
*/
|
|
25
|
+
export declare function signConvEvent<E extends UnsignedConvEvent>(identity: Identity, unsigned: E): E & {
|
|
26
|
+
sig: string;
|
|
27
|
+
};
|
|
28
|
+
/** True only for a valid signature by `event.by.device` over the event without `sig`. Never throws. */
|
|
29
|
+
export declare function convEventSigned(event: ConvEvent): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Admits `raw` (`admitConvEvent`) and checks its signature over the raw value (`bad_signature`). Membership and
|
|
32
|
+
* authority are `ConvLog.apply`'s. Throws only MessagingError.
|
|
33
|
+
*/
|
|
34
|
+
export declare function verifyConvEvent(raw: unknown): ConvEvent;
|
|
35
|
+
/** Freezes a JSON value and everything in it, iteratively. */
|
|
36
|
+
export declare function deepFreeze<T>(value: T): T;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { PersonId } from "../schema/common.js";
|
|
2
|
+
import { type ConvId, ParticipantId } from "../schema/conversation.js";
|
|
3
|
+
/**
|
|
4
|
+
* The id of the dm between `a` and `b`: `"d_" + base32lower(SHA-256(DOMAIN.dm ‖ JCS([a, b] sorted)))[0..26]`. Both
|
|
5
|
+
* ends derive the same id, so a dm has no create race. `a` may equal `b`: a person's note to themselves.
|
|
6
|
+
*/
|
|
7
|
+
export declare function dmConvId(a: ParticipantId, b: ParticipantId): ConvId;
|
|
8
|
+
/** The id of `person`'s own `self` conversation: `"s_" + base32lower(SHA-256(DOMAIN.self ‖ JCS(person)))[0..26]`. */
|
|
9
|
+
export declare function selfConvId(person: PersonId): ConvId;
|
|
10
|
+
/** The person a participant is, or belongs to. */
|
|
11
|
+
export declare function participantPerson(p: ParticipantId): PersonId;
|
|
12
|
+
/** The agent's session name, for an agent participant. */
|
|
13
|
+
export declare function participantAgent(p: ParticipantId): string | undefined;
|
|
14
|
+
/** The participant an event's `by` names: `person`, or `person/agent`. */
|
|
15
|
+
export declare function participantOf(by: {
|
|
16
|
+
person: PersonId;
|
|
17
|
+
agent?: string | undefined;
|
|
18
|
+
}): ParticipantId;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MachineId, PersonId } from "../schema/common.js";
|
|
2
|
+
import { type LinkRecord, type MsgBody, type ReaderId } from "../schema/conversation.js";
|
|
3
|
+
/** The links in force: for each link id the record written last (records are in `seq` order), unless revoked. */
|
|
4
|
+
export declare function currentLinks(records: readonly LinkRecord[]): LinkRecord[];
|
|
5
|
+
/**
|
|
6
|
+
* Checks a line's `via` against the author's links, as the author's own devices can (other members cannot see the
|
|
7
|
+
* person's links): only a person writes from a linked channel, never an agent, and only through a link of that
|
|
8
|
+
* channel that the authoring device hosts and that is in force. Throws `forbidden`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function checkVia(body: MsgBody, by: {
|
|
11
|
+
person: PersonId;
|
|
12
|
+
device: MachineId;
|
|
13
|
+
agent?: string;
|
|
14
|
+
}, links: readonly LinkRecord[]): void;
|
|
15
|
+
/** The reader id of a link's channel provider, for the audience guard: `ext:<channel>:<link id>`. */
|
|
16
|
+
export declare function linkReader(link: Pick<LinkRecord, "channel" | "id">): ReaderId;
|