@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,223 +1,40 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The local API's surface methods (spec §8.2; design §7.5): what agents reach through their tools
|
|
3
|
-
* and what people reach through the CLI and the TUI, by conversation.
|
|
4
|
-
*
|
|
5
|
-
* - Agent tier (`run/agent.sock`): `contacts.list`, `msg.send`, `item.read`, `conv.read`, `search`.
|
|
6
|
-
* - Human tier (`run/admin.sock`): `conv.list`, `conv.read`, `conv.markRead`, `conv.subscribe`,
|
|
7
|
-
* `person.send`, `people.search`, `people.get`.
|
|
8
|
-
*
|
|
9
|
-
* A conversation is named by a `ConversationRef`: `dm:<person>` (this person's DM with another),
|
|
10
|
-
* `channel:<name>` (a channel this person is in, by its local name) or `agent:<person>/<agent>`
|
|
11
|
-
* (a local session's mail and tasks with that agent, on any of the person's devices).
|
|
12
|
-
*/
|
|
1
|
+
/** Discovery and task-envelope reads. Conversation methods live in conversations.ts. */
|
|
13
2
|
import { z } from "../../../vendor/zod/index.js";
|
|
14
|
-
import { Message
|
|
15
|
-
import {
|
|
16
|
-
import { DelegationScope } from "../delegation.js";
|
|
17
|
-
import { Email } from "../directory-frames.js";
|
|
3
|
+
import { Message } from "../a2a.js";
|
|
4
|
+
import { DeliveryStage, Millis, PersonId, Ulid } from "../common.js";
|
|
18
5
|
import { ErrorCode } from "../errors.js";
|
|
19
6
|
import { PersonReach } from "./consent.js";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*/
|
|
26
|
-
const METADATA_BYTES = 16_384;
|
|
27
|
-
/** A conversation: `dm:<person>`, `channel:<name>` or `agent:<person>/<agent>`. */
|
|
28
|
-
export const ConversationRef = z
|
|
29
|
-
.string()
|
|
30
|
-
.regex(/^(?:dm:(?:[a-z][a-z0-9-]{0,31}|p_[a-z2-7]{26})|channel:[a-z][a-z0-9-]{0,31}|agent:(?:[a-z][a-z0-9-]{0,31}|p_[a-z2-7]{26})\/[a-z0-9][a-z0-9._-]{0,63})$/, "dm:<person>, channel:<name> or agent:<person>/<agent>");
|
|
31
|
-
/** A place in a conversation: a line's `seq` in a DM or a channel, an entry's `{at, id}` with an agent. */
|
|
32
|
-
export const ConvPlace = z.union([ChatSeq, ConvCursor]);
|
|
33
|
-
/** One conversation this person has (DMs with lines, channels it is in) and this device's read state. */
|
|
34
|
-
export const ConvSummary = z.object({
|
|
35
|
-
conversation: RoomRef,
|
|
36
|
-
kind: z.enum(["dm", "channel"]),
|
|
37
|
-
lastAt: Millis.optional(),
|
|
38
|
-
/** A channel held under a local name (another channel here already had its name) carries its own name. */
|
|
39
|
-
name: z.string().max(32).optional(),
|
|
40
|
-
/** The newest line's seq; absent in a channel with no lines. */
|
|
41
|
-
lastSeq: ChatSeq.optional(),
|
|
42
|
-
/** 0 when nothing is marked read. */
|
|
43
|
-
readUpTo: ChatSeq,
|
|
44
|
-
/** Lines from others after `readUpTo`. */
|
|
45
|
-
unread: z.number().int().nonnegative(),
|
|
7
|
+
import { ConvId, ConvKind, ParticipantId } from "../conversation.js";
|
|
8
|
+
import { DirectoryEntry, InboxItem, SendTarget, SessionKey } from "./shapes.js";
|
|
9
|
+
/** Verified discovery metadata, without transcript or read-state placeholders. */
|
|
10
|
+
export const ContactConversation = z.strictObject({
|
|
11
|
+
cid: ConvId, kind: ConvKind, name: z.string().max(200).optional(), dmWith: ParticipantId.optional(),
|
|
46
12
|
});
|
|
47
|
-
/** A person of the realm, as verified (a roster realm: a team person). */
|
|
48
13
|
export const PersonSummary = z.object({
|
|
49
14
|
person: PersonId,
|
|
50
|
-
/** The display name; a roster realm has only the person name. Untrusted text in a hosted realm. */
|
|
51
15
|
name: z.string().max(200),
|
|
52
|
-
/** What a person types to address them. */
|
|
53
16
|
handle: z.string().max(64),
|
|
54
|
-
/** `same`: in this device's person's org (a roster realm: the team). */
|
|
55
17
|
org: z.enum(["same", "other", "none"]),
|
|
56
|
-
/** The weakest vouch among their current devices. */
|
|
57
18
|
vouched: z.enum(["jelly", "device", "service", "admin"]),
|
|
58
19
|
});
|
|
59
20
|
export const SURFACE_METHODS = {
|
|
60
|
-
/**
|
|
61
|
-
* Agent tier. Who this session can reach: every known agent session (`entries`, as `directory.list`
|
|
62
|
-
* did in 0.1), and this person's conversations with their read state (`conversations`, as
|
|
63
|
-
* `conv.list` returns them).
|
|
64
|
-
*/
|
|
65
21
|
"contacts.list": {
|
|
66
22
|
params: z.strictObject({ person: PersonId.optional(), includeOffline: z.boolean().optional() }),
|
|
67
|
-
|
|
68
|
-
* `reach`: for each other person among `entries` and the DMs, and each person whose card this device holds, what they may
|
|
69
|
-
* be sent, from their card or the defaults of the relation (spec §6.9, "Contact cards").
|
|
70
|
-
*/
|
|
71
|
-
result: z.object({ entries: z.array(DirectoryEntry), conversations: z.array(ConvSummary), reach: z.array(PersonReach) }),
|
|
23
|
+
result: z.object({ entries: z.array(DirectoryEntry), conversations: z.array(ContactConversation), reach: z.array(PersonReach) }),
|
|
72
24
|
},
|
|
73
|
-
/**
|
|
74
|
-
* Agent tier. One send from the session `key` (spec §8.2): mail or a task to an agent
|
|
75
|
-
* (`person/agent[@label]`), a line to a person (`<person>`), or a post to a channel (`#<channel>`).
|
|
76
|
-
* A line or a post is attributed to the session; it takes one text part of at most
|
|
77
|
-
* `LIMITS.chatChars`, `kind: "message"`, and `replyTo` as the thread (the thread's first line),
|
|
78
|
-
* and none of the A2A fields. `idempotencyKey` makes any send idempotent.
|
|
79
|
-
*/
|
|
80
|
-
"msg.send": {
|
|
81
|
-
params: z.strictObject({
|
|
82
|
-
key: SessionKey,
|
|
83
|
-
to: z.union([Address, z.string().regex(/^#[a-z][a-z0-9-]{0,31}$/, "#<channel>")]),
|
|
84
|
-
kind: z.enum(["message", "task"]),
|
|
85
|
-
parts: z.array(Part).min(1).max(LIMITS.partsPerMessage),
|
|
86
|
-
contextId: z.string().max(128).optional(),
|
|
87
|
-
taskId: Ulid.optional().describe("for kind=message: the task this message continues (INPUT_REQUIRED replies)"),
|
|
88
|
-
replyTo: Ulid.optional(),
|
|
89
|
-
idempotencyKey: z.string().min(1).max(200).optional(),
|
|
90
|
-
/** A2A passthrough. The daemon generates messageId when omitted and always overwrites metadata[URI.provenance]. */
|
|
91
|
-
messageId: z.string().min(1).max(128).optional(),
|
|
92
|
-
metadata: z
|
|
93
|
-
.record(z.string(), z.unknown())
|
|
94
|
-
.refine((m) => new TextEncoder().encode(JSON.stringify(m)).length <= METADATA_BYTES, { message: `metadata is limited to ${METADATA_BYTES} bytes of JSON` })
|
|
95
|
-
.optional(),
|
|
96
|
-
extensions: z.array(z.string().min(1).max(256)).max(16).optional(),
|
|
97
|
-
referenceTaskIds: z.array(Ulid).max(16).optional(),
|
|
98
|
-
wake: z.boolean().optional().describe("hint that the recipient may wake an idle session; default true"),
|
|
99
|
-
/**
|
|
100
|
-
* Only on a line to the session's own person (`<own person>`, spec §6.9 "Message requests"): the agent asks its
|
|
101
|
-
* person to send a message request to this exact email. The daemon files it for the person to answer; only the
|
|
102
|
-
* person sends the request.
|
|
103
|
-
*/
|
|
104
|
-
request: z.strictObject({ contact: Email }).optional(),
|
|
105
|
-
/**
|
|
106
|
-
* kind=task only (spec §6.7): delegate on behalf of a task this session executes. The daemon extends that
|
|
107
|
-
* task's chain to each recipient, narrowed to `scope` and `maxDepth` when given. Without it, a task starts a new chain.
|
|
108
|
-
*/
|
|
109
|
-
onBehalfOf: z
|
|
110
|
-
.strictObject({
|
|
111
|
-
taskId: Ulid.describe("a task this session executes"),
|
|
112
|
-
scope: z.array(DelegationScope).max(LIMITS.delegationScopes).optional().describe("narrow the scope; never wider than the task's"),
|
|
113
|
-
maxDepth: z.number().int().min(1).max(LIMITS.delegationDepth).optional().describe("narrow the depth; never above the task's"),
|
|
114
|
-
})
|
|
115
|
-
.optional(),
|
|
116
|
-
}),
|
|
117
|
-
result: z.object({ results: z.array(SendResult) }),
|
|
118
|
-
},
|
|
119
|
-
/**
|
|
120
|
-
* Agent tier. One item of the session `key` by id: something it received (`received`: returns the
|
|
121
|
-
* full message, marks it read and sends a read receipt, as `inbox.read` did in 0.1), or something
|
|
122
|
-
* it sent to an agent (`sent`: how far it got, as `message.status` did). `not_found` otherwise.
|
|
123
|
-
*/
|
|
25
|
+
/** A received item, or the delivery records of a sent task envelope. */
|
|
124
26
|
"item.read": {
|
|
125
|
-
params: z.strictObject({ key: SessionKey, id: Ulid }),
|
|
27
|
+
params: z.strictObject({ key: SessionKey, id: Ulid, cid: ConvId.optional() }),
|
|
126
28
|
result: z.discriminatedUnion("kind", [
|
|
127
29
|
z.object({ kind: z.literal("received"), item: InboxItem, message: Message }),
|
|
128
30
|
z.object({
|
|
129
|
-
kind: z.literal("sent"),
|
|
130
|
-
envelopeId: Ulid,
|
|
131
|
-
to: SendTarget,
|
|
132
|
-
stage: DeliveryStage,
|
|
31
|
+
kind: z.literal("sent"), envelopeId: Ulid, to: SendTarget, stage: DeliveryStage,
|
|
133
32
|
history: z.array(z.object({ stage: DeliveryStage, at: Millis, code: ErrorCode.optional(), note: z.string().optional() })),
|
|
134
33
|
}),
|
|
135
34
|
]),
|
|
136
35
|
},
|
|
137
|
-
/**
|
|
138
|
-
* Agent and human tiers. A page of one conversation, oldest first. With no cursor, the latest
|
|
139
|
-
* `limit` (default 30 with an agent, 50 in a DM or a channel); with `before`, the latest `limit`
|
|
140
|
-
* before that place; with `after`, the earliest `limit` after it. `more`: entries remain past the
|
|
141
|
-
* page in the direction read. Changes no stage and sends no receipt.
|
|
142
|
-
* - `dm:` and `channel:` give `lines`; `thread` narrows to a thread's first line and its replies;
|
|
143
|
-
* the cursor is a line's `seq`.
|
|
144
|
-
* - `agent:<person>/<agent>` needs the session `key` and gives `entries`: mail either way and tasks
|
|
145
|
-
* either way between that session and the agent; the cursor is an entry's `{at, id}`.
|
|
146
|
-
*/
|
|
147
|
-
"conv.read": {
|
|
148
|
-
params: z
|
|
149
|
-
.strictObject({
|
|
150
|
-
key: SessionKey.optional(),
|
|
151
|
-
conversation: ConversationRef,
|
|
152
|
-
thread: Ulid.optional(),
|
|
153
|
-
before: ConvPlace.optional(),
|
|
154
|
-
after: ConvPlace.optional(),
|
|
155
|
-
limit: z.number().int().min(1).max(100).optional(),
|
|
156
|
-
})
|
|
157
|
-
.refine((p) => p.before === undefined || p.after === undefined, { message: "before and after do not combine" }),
|
|
158
|
-
result: z.discriminatedUnion("kind", [
|
|
159
|
-
z.object({ kind: z.literal("lines"), lines: z.array(ChatLine), more: z.boolean() }),
|
|
160
|
-
z.object({ kind: z.literal("agent"), entries: z.array(ConvEntry), more: z.boolean() }),
|
|
161
|
-
]),
|
|
162
|
-
},
|
|
163
|
-
/** Agent tier. Full-text search over the DM and channel lines `conv.read` can read, newest first. */
|
|
164
|
-
search: {
|
|
165
|
-
params: z.strictObject({ query: z.string().min(1).max(200), conversation: RoomRef.optional(), limit: z.number().int().min(1).max(50).optional() }),
|
|
166
|
-
result: z.object({ hits: z.array(ChatLine.extend({ conversation: RoomRef })) }),
|
|
167
|
-
},
|
|
168
|
-
/**
|
|
169
|
-
* Human tier. The channels this person is in and the DMs with lines, newest first, at most `limit`
|
|
170
|
-
* (default 100), each with this device's read state.
|
|
171
|
-
*/
|
|
172
|
-
"conv.list": {
|
|
173
|
-
params: z.strictObject({ limit: z.number().int().min(1).max(200).optional() }),
|
|
174
|
-
result: z.object({ conversations: z.array(ConvSummary) }),
|
|
175
|
-
},
|
|
176
|
-
/**
|
|
177
|
-
* Human tier. Marks a DM or a channel read on this device up to `upTo` (a line's `seq`): the cursor
|
|
178
|
-
* only moves forward, and never past the newest seq here. Returns the cursor and what is still unread.
|
|
179
|
-
*/
|
|
180
|
-
"conv.markRead": {
|
|
181
|
-
params: z.strictObject({ conversation: RoomRef, upTo: ChatSeq }),
|
|
182
|
-
result: z.object({ readUpTo: ChatSeq, unread: z.number().int().nonnegative() }),
|
|
183
|
-
},
|
|
184
|
-
/**
|
|
185
|
-
* Human tier. `conv` events go only to connections that call this (spec §6.8). With `since` (the
|
|
186
|
-
* `seq` of the last event the client handled), the daemon first replays one event per line changed
|
|
187
|
-
* after it, oldest first, or sends none and answers `gap: true` when it cannot replay them all; the
|
|
188
|
-
* client then reloads with `conv.list` and `conv.read`. `seq` is the newest seq at the call.
|
|
189
|
-
*/
|
|
190
|
-
"conv.subscribe": { params: z.strictObject({ since: ChatSeq.optional() }), result: z.object({ seq: ChatSeq, gap: z.boolean() }) },
|
|
191
|
-
/**
|
|
192
|
-
* Human tier. A line from the person (no agent) to a DM or a channel this person is in. `thread` is
|
|
193
|
-
* the `id` of the thread's first line. `clientId` makes the call idempotent: a repeat with the same
|
|
194
|
-
* `clientId` and the same conversation, thread and text returns the first line's result and sends nothing.
|
|
195
|
-
*/
|
|
196
|
-
"person.send": {
|
|
197
|
-
params: z.strictObject({
|
|
198
|
-
conversation: RoomRef,
|
|
199
|
-
text: ChatText.min(1),
|
|
200
|
-
thread: Ulid.optional(),
|
|
201
|
-
clientId: z.string().min(1).max(200).optional(),
|
|
202
|
-
}),
|
|
203
|
-
result: z.object({ id: Ulid, conversation: RoomRef }),
|
|
204
|
-
},
|
|
205
|
-
/** Human tier. The persons of the realm whose handle starts with `query` (all with none), at most `limit` (default 50). */
|
|
206
36
|
"people.search": {
|
|
207
37
|
params: z.strictObject({ query: z.string().max(100).optional(), limit: z.number().int().min(1).max(200).optional() }),
|
|
208
38
|
result: z.object({ people: z.array(PersonSummary) }),
|
|
209
39
|
},
|
|
210
|
-
/** Human tier. One person and their current devices, as verified. `not_found` for a person this realm does not know. */
|
|
211
|
-
"people.get": {
|
|
212
|
-
params: z.strictObject({ person: PersonId }),
|
|
213
|
-
/** A device's `away`: its latest presence, still fresh, says no window of the person is visible on it (spec §6.5). */
|
|
214
|
-
result: PersonSummary.extend({
|
|
215
|
-
devices: z.array(z.object({ device: MachineId, label: MachineLabel, self: z.boolean(), away: z.literal(true).optional() })),
|
|
216
|
-
/**
|
|
217
|
-
* Their devices added, removed or replaced in the last 7 days, newest first, at most 10 (design §2.8: "Bob's
|
|
218
|
-
* devices changed: added 'bob-vps' (vouched by Jelly ID)"). Absent for this device's own person and when none.
|
|
219
|
-
*/
|
|
220
|
-
deviceChanges: z.array(DeviceChangeView).max(10).optional(),
|
|
221
|
-
}),
|
|
222
|
-
},
|
|
223
40
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Retained task envelopes, independent of conversation lines. */
|
|
2
|
+
import { z } from "../../../vendor/zod/index.js";
|
|
3
|
+
import { Part } from "../a2a.js";
|
|
4
|
+
import { Address, LIMITS, Ulid } from "../common.js";
|
|
5
|
+
import { DelegationScope } from "../delegation.js";
|
|
6
|
+
import { SendResult, SessionKey } from "./shapes.js";
|
|
7
|
+
const fields = {
|
|
8
|
+
key: SessionKey,
|
|
9
|
+
to: Address,
|
|
10
|
+
parts: z.array(Part).min(1).max(LIMITS.partsPerMessage),
|
|
11
|
+
contextId: z.string().max(128).optional(),
|
|
12
|
+
replyTo: Ulid.optional(),
|
|
13
|
+
idempotencyKey: z.string().min(1).max(200).optional(),
|
|
14
|
+
messageId: z.string().min(1).max(128).optional(),
|
|
15
|
+
metadata: z.record(z.string(), z.unknown()).refine((m) => new TextEncoder().encode(JSON.stringify(m)).length <= 16_384, { message: "metadata is limited to 16384 bytes of JSON" }).optional(),
|
|
16
|
+
extensions: z.array(z.string().min(1).max(256)).max(16).optional(),
|
|
17
|
+
referenceTaskIds: z.array(Ulid).max(16).optional(),
|
|
18
|
+
wake: z.boolean().optional(),
|
|
19
|
+
};
|
|
20
|
+
const result = z.object({ results: z.array(SendResult) });
|
|
21
|
+
export const TASK_SEND_METHODS = {
|
|
22
|
+
"task.create": {
|
|
23
|
+
params: z.strictObject({ ...fields, onBehalfOf: z.strictObject({
|
|
24
|
+
taskId: Ulid,
|
|
25
|
+
scope: z.array(DelegationScope).max(LIMITS.delegationScopes).optional(),
|
|
26
|
+
maxDepth: z.number().int().min(1).max(LIMITS.delegationDepth).optional(),
|
|
27
|
+
}).optional() }),
|
|
28
|
+
result,
|
|
29
|
+
},
|
|
30
|
+
"task.continue": { params: z.strictObject({ ...fields, taskId: Ulid }), result },
|
|
31
|
+
};
|
|
@@ -15,21 +15,25 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Errors: standard JSON-RPC codes for transport/schema problems (-32700, -32600, -32601, -32602,
|
|
17
17
|
* -32603); every Messaging error uses code -32000 with `data: { code: ErrorCode }`.
|
|
18
|
-
* The daemon pushes notifications `{"jsonrpc":"2.0","method":"event","params":Event}
|
|
19
|
-
* to
|
|
20
|
-
* `events.subscribe` for its key (or to all of them, for events without a key).
|
|
18
|
+
* The daemon pushes notifications `{"jsonrpc":"2.0","method":"event","params":Event}` to connections
|
|
19
|
+
* subscribed to the event's key. Notice subscriptions are human-only. Conversations use `conv.sync`.
|
|
21
20
|
*/
|
|
22
21
|
import { z } from "../../vendor/zod/index.js";
|
|
23
22
|
import { DEVICE_METHODS, DeviceStatus, LoginPurpose } from "./ipc/device.js";
|
|
24
23
|
import { Artifact, Message, Part, TaskState, TaskStatus } from "./a2a.js";
|
|
25
|
-
import { AgentKind,
|
|
24
|
+
import { AgentKind, DeliveryStage, DeliveryTier, EncKey, Hash, MachineId, MachineLabel, MemberRole, Millis, PersonId, PersonName, RelayUrl, SessionAddress, SessionName, SessionState, TeamId, TeamName, Ulid, LIMITS, } from "./common.js";
|
|
26
25
|
import { ErrorCode } from "./errors.js";
|
|
27
|
-
import {
|
|
26
|
+
import { ActionToken, InboxItem, PersonTask, SessionKey, TASK_SIGN_IN_TOOL_CHARS, TaskQuestion, TaskRecord } from "./ipc/shapes.js";
|
|
28
27
|
import { SURFACE_METHODS } from "./ipc/surfaces.js";
|
|
29
28
|
import { Member } from "./roster.js";
|
|
30
29
|
import { CONSENT_METHODS } from "./ipc/consent.js";
|
|
31
30
|
import { JELLY_METHODS, Notice } from "./ipc/jelly.js";
|
|
32
31
|
import { Jws } from "./jelly.js";
|
|
32
|
+
import { LIVE_CONVERSATION_METHODS, CONVERSATION_TIERS } from "./ipc/conversations.js";
|
|
33
|
+
import { TASK_SEND_METHODS } from "./ipc/tasks.js";
|
|
34
|
+
import { ConvId } from "./conversation.js";
|
|
35
|
+
const conversationTier = (tier) => Object.keys(LIVE_CONVERSATION_METHODS)
|
|
36
|
+
.filter((m) => CONVERSATION_TIERS[m] === tier);
|
|
33
37
|
export * from "./ipc/shapes.js";
|
|
34
38
|
export * from "./ipc/surfaces.js";
|
|
35
39
|
export * from "./ipc/jelly.js";
|
|
@@ -120,6 +124,7 @@ export const InviteSummary = z.object({
|
|
|
120
124
|
/** Method table: name → params/result schemas. The daemon validates params; clients validate results. */
|
|
121
125
|
export const IPC_METHODS = {
|
|
122
126
|
...DEVICE_METHODS,
|
|
127
|
+
...TASK_SEND_METHODS,
|
|
123
128
|
"daemon.status": { params: Empty, result: DaemonStatus },
|
|
124
129
|
/** Human CLI only: replies, then shuts the daemon down cleanly. */
|
|
125
130
|
"daemon.stop": { params: Empty, result: Ok },
|
|
@@ -166,11 +171,11 @@ export const IPC_METHODS = {
|
|
|
166
171
|
* and ack after (`inbox.ack`), so a crash in between leaves the item unread.
|
|
167
172
|
*/
|
|
168
173
|
"inbox.get": {
|
|
169
|
-
params: z.strictObject({ key: SessionKey, id: Ulid, message: z.boolean().optional() }),
|
|
174
|
+
params: z.strictObject({ key: SessionKey, id: Ulid, cid: ConvId.optional(), message: z.boolean().optional() }),
|
|
170
175
|
result: z.object({ item: InboxItem, message: Message.optional() }),
|
|
171
176
|
},
|
|
172
177
|
"inbox.ack": {
|
|
173
|
-
params: z.strictObject({ key: SessionKey, id: Ulid, stage: z.enum(["handed_to_agent", "read"]) }),
|
|
178
|
+
params: z.strictObject({ key: SessionKey, id: Ulid, cid: ConvId.optional(), stage: z.enum(["handed_to_agent", "read"]) }),
|
|
174
179
|
result: Ok,
|
|
175
180
|
},
|
|
176
181
|
"task.list": {
|
|
@@ -201,11 +206,53 @@ export const IPC_METHODS = {
|
|
|
201
206
|
artifact: Artifact.optional(),
|
|
202
207
|
append: z.boolean().optional(),
|
|
203
208
|
lastChunk: z.boolean().optional(),
|
|
204
|
-
|
|
209
|
+
/** With `TASK_STATE_AUTH_REQUIRED` only: the tool whose sign-in the task waits on (spec §6.2, "Sign-in"). */
|
|
210
|
+
signIn: z.strictObject({ tool: z.string().min(1).max(TASK_SIGN_IN_TOOL_CHARS) }).optional(),
|
|
211
|
+
}).refine((p) => p.signIn === undefined || p.state === "TASK_STATE_AUTH_REQUIRED", "signIn goes with TASK_STATE_AUTH_REQUIRED"),
|
|
205
212
|
result: TaskRecord,
|
|
206
213
|
},
|
|
214
|
+
/**
|
|
215
|
+
* Agent tier, from the executing session's runtime (never a model tool). Ties a task the session `key` runs (its
|
|
216
|
+
* executor side), in `AUTH_REQUIRED` or `INPUT_REQUIRED`, to the host's own pending approval or question it waits on;
|
|
217
|
+
* `question: null` unties it. `sessionId` must be the session's own, when its registration named one (`host`).
|
|
218
|
+
* Held in memory only, shown only to the person's own clients (admin socket, no key), never sent (spec §8.2).
|
|
219
|
+
*/
|
|
220
|
+
"task.question": {
|
|
221
|
+
params: z.strictObject({ key: SessionKey, taskId: Ulid, question: TaskQuestion.nullable() }),
|
|
222
|
+
result: z.object({ ok: z.literal(true) }),
|
|
223
|
+
},
|
|
224
|
+
/**
|
|
225
|
+
* Human tier, admin socket only, no session key. The tasks this person's agents on this device are in, as requester
|
|
226
|
+
* or executor, across their sessions, most recently updated first: the person's task view (spec §8.2, "The person's
|
|
227
|
+
* tasks"). Never another person's tasks: this device holds only its own sessions' sides.
|
|
228
|
+
*/
|
|
229
|
+
"tasks.list": {
|
|
230
|
+
params: z.strictObject({
|
|
231
|
+
role: z.enum(["requester", "executor"]).optional(),
|
|
232
|
+
state: TaskState.optional(),
|
|
233
|
+
session: SessionName.optional().describe("only this agent's tasks"),
|
|
234
|
+
cursor: z.string().min(1).max(200).optional().describe("opaque; from a previous nextCursor"),
|
|
235
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
236
|
+
}),
|
|
237
|
+
result: z.object({
|
|
238
|
+
tasks: z.array(PersonTask),
|
|
239
|
+
nextCursor: z.string().max(200).optional().describe("absent when there are no more pages"),
|
|
240
|
+
total: z.number().int().nonnegative(),
|
|
241
|
+
}),
|
|
242
|
+
},
|
|
243
|
+
/** Human tier. One side of a task of this person's agents; `role` when this device holds both sides. */
|
|
244
|
+
"tasks.get": {
|
|
245
|
+
params: z.strictObject({ taskId: Ulid, role: z.enum(["requester", "executor"]).optional() }),
|
|
246
|
+
result: PersonTask,
|
|
247
|
+
},
|
|
248
|
+
/**
|
|
249
|
+
* As the requester session (`key`), or, on the human tier, as the person with the task's `cancel` action (`token`,
|
|
250
|
+
* spec §8.2, "Action tokens"): exactly one of the two.
|
|
251
|
+
*/
|
|
207
252
|
"task.cancel": {
|
|
208
|
-
params: z
|
|
253
|
+
params: z
|
|
254
|
+
.strictObject({ key: SessionKey.optional(), taskId: Ulid, reason: z.string().max(1000).optional(), token: ActionToken.optional() })
|
|
255
|
+
.refine((p) => (p.key === undefined) !== (p.token === undefined), "give exactly one of key or token"),
|
|
209
256
|
result: TaskRecord,
|
|
210
257
|
},
|
|
211
258
|
"events.subscribe": { params: z.strictObject({ keys: z.array(SessionKey).min(1).max(256) }), result: Ok },
|
|
@@ -291,24 +338,10 @@ export const IPC_METHODS = {
|
|
|
291
338
|
* while they are pending. Private keys stay in the daemon.
|
|
292
339
|
*/
|
|
293
340
|
"team.prepare": { params: Empty, result: z.object({ machine: MachineId, enc: EncKey }) },
|
|
294
|
-
/** Creates a channel with this person and `persons` (at most `LIMITS.channelPersons` together). */
|
|
295
|
-
"channel.create": {
|
|
296
|
-
params: z.strictObject({ name: ChannelName, persons: z.array(PersonId).max(LIMITS.channelPersons) }),
|
|
297
|
-
result: z.object({ id: Ulid, name: ChannelName }),
|
|
298
|
-
},
|
|
299
|
-
"channel.invite": {
|
|
300
|
-
params: z.strictObject({ name: ChannelName, persons: z.array(PersonId).min(1).max(LIMITS.channelPersons) }),
|
|
301
|
-
result: Ok,
|
|
302
|
-
},
|
|
303
|
-
"channel.leave": { params: z.strictObject({ name: ChannelName }), result: Ok },
|
|
304
|
-
/** Removes other persons from a channel; only its creator or a team admin may (spec §6.8). */
|
|
305
|
-
"channel.remove": {
|
|
306
|
-
params: z.strictObject({ name: ChannelName, persons: z.array(PersonId).min(1).max(LIMITS.channelPersons) }),
|
|
307
|
-
result: Ok,
|
|
308
|
-
},
|
|
309
341
|
...CONSENT_METHODS,
|
|
310
342
|
...JELLY_METHODS,
|
|
311
343
|
...SURFACE_METHODS,
|
|
344
|
+
...LIVE_CONVERSATION_METHODS,
|
|
312
345
|
};
|
|
313
346
|
/**
|
|
314
347
|
* The agent tier (design §7.5): the methods an agent-facing client may call, and exactly what the
|
|
@@ -325,17 +358,18 @@ export const AGENT_METHODS = [
|
|
|
325
358
|
"session.unregister",
|
|
326
359
|
"events.subscribe",
|
|
327
360
|
"contacts.list",
|
|
328
|
-
"msg.send",
|
|
329
361
|
"inbox.list",
|
|
330
362
|
"inbox.get",
|
|
331
363
|
"inbox.ack",
|
|
332
364
|
"item.read",
|
|
333
|
-
"
|
|
334
|
-
"
|
|
365
|
+
...conversationTier("agent"),
|
|
366
|
+
"task.create",
|
|
367
|
+
"task.continue",
|
|
335
368
|
"task.list",
|
|
336
369
|
"task.get",
|
|
337
370
|
"task.update",
|
|
338
371
|
"task.cancel",
|
|
372
|
+
"task.question",
|
|
339
373
|
];
|
|
340
374
|
/**
|
|
341
375
|
* The human tier (design §7.5): methods that act as the person, on `run/admin.sock` only: their
|
|
@@ -343,35 +377,30 @@ export const AGENT_METHODS = [
|
|
|
343
377
|
* `conv.read` is in both tiers.
|
|
344
378
|
*/
|
|
345
379
|
export const HUMAN_METHODS = [
|
|
346
|
-
"
|
|
347
|
-
"
|
|
348
|
-
"conv.markRead",
|
|
349
|
-
"conv.subscribe",
|
|
350
|
-
"person.send",
|
|
380
|
+
...conversationTier("human"),
|
|
381
|
+
...conversationTier("agent"),
|
|
351
382
|
"people.search",
|
|
352
|
-
"people.get",
|
|
353
383
|
"grant.list",
|
|
354
384
|
"grant.set",
|
|
355
385
|
"grant.set.preview",
|
|
386
|
+
"reach.get",
|
|
387
|
+
"reach.set",
|
|
388
|
+
"reach.set.preview",
|
|
356
389
|
"request.list",
|
|
357
390
|
"request.answer",
|
|
358
391
|
"request.answer.preview",
|
|
359
392
|
"contact.request",
|
|
360
393
|
"contact.request.preview",
|
|
361
394
|
"card.get",
|
|
362
|
-
"agents.list",
|
|
363
|
-
"agents.read",
|
|
364
395
|
"notice.subscribe",
|
|
365
396
|
"notice.list",
|
|
366
397
|
"notice.ack",
|
|
398
|
+
"tasks.list",
|
|
399
|
+
"tasks.get",
|
|
367
400
|
"device.approve.preview",
|
|
368
401
|
"device.remove.preview",
|
|
369
402
|
"identity.replace.preview",
|
|
370
403
|
"directory.erase.preview",
|
|
371
|
-
"channel.create",
|
|
372
|
-
"channel.invite",
|
|
373
|
-
"channel.leave",
|
|
374
|
-
"channel.remove",
|
|
375
404
|
];
|
|
376
405
|
/**
|
|
377
406
|
* The admin tier (design §7.5): this device and the daemon, and in a roster realm the team, on
|
|
@@ -434,13 +463,6 @@ export const IpcEvent = z.discriminatedUnion("type", [
|
|
|
434
463
|
/** What `contacts.list` returns changed: a presence, a session, the roster. */
|
|
435
464
|
z.object({ type: z.literal("contacts") }),
|
|
436
465
|
z.object({ type: z.literal("relay"), connected: z.boolean() }),
|
|
437
|
-
/**
|
|
438
|
-
* A line was stored in a DM or a channel, a channel this person is in changed, or the delivery state
|
|
439
|
-
* of an outgoing line changed. Only for connections that called `conv.subscribe`. The text stays in
|
|
440
|
-
* the transcript. `seq` rises with every event; a client resumes from the last one it handled
|
|
441
|
-
* (`conv.subscribe {since}`).
|
|
442
|
-
*/
|
|
443
|
-
z.object({ type: z.literal("conv"), conversation: RoomRef, id: Ulid, person: PersonId, seq: ChatSeq }),
|
|
444
466
|
/**
|
|
445
467
|
* A device's sign-in through the browser changed state (spec §15.9): its state and, when it failed, the code. Never
|
|
446
468
|
* the code to type, the link or a token; `device.login.status` gives the rest on the admin socket.
|
|
@@ -7,7 +7,7 @@ export { encodeTicket } from "./roster.js";
|
|
|
7
7
|
export { grantHash } from "./delegation.js";
|
|
8
8
|
export { signJellyToken } from "./directory/jelly-issuer.js";
|
|
9
9
|
export { jellyHostDouble } from "./jelly-host-double.js";
|
|
10
|
-
export { sealConvMetaWith, sealConvMsgWith,
|
|
10
|
+
export { sealConvMetaWith, sealConvMsgWith, sealConvStateWith, sealKeyShareWith } from "./conversation/seal.js";
|
|
11
11
|
/**
|
|
12
12
|
* Narrows a result whose `kind` tells its shape (`item.read`, `conv.read`) to the expected kind, and
|
|
13
13
|
* throws with both kinds named otherwise: `client.call("item.read", p).then(asKind("received"))`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
-
import { ErrorCode, IPC_METHODS, IPC_PROTOCOL, IpcEvent, IpcHello, LIMITS, MESSAGING_ERROR_RPC_CODE, MessagingError, VERSION, parseWire, } from "../core/index.js";
|
|
3
|
+
import { ErrorCode, CONVERSATION_METHODS, IPC_METHODS, IPC_PROTOCOL, IpcEvent, IpcHello, LIMITS, MESSAGING_ERROR_RPC_CODE, MessagingError, VERSION, parseWire, } from "../core/index.js";
|
|
4
4
|
import { encodeLine, LineDecoder } from "./framing.js";
|
|
5
5
|
import { configuredDaemonUid, IS_WINDOWS, messagingHome, socketPath as defaultSocketPath } from "./paths.js";
|
|
6
6
|
import { readPointer } from "./pipes.js";
|
|
@@ -153,6 +153,13 @@ export class MessagingClient {
|
|
|
153
153
|
return this.isClosed;
|
|
154
154
|
}
|
|
155
155
|
call(method, params, options = {}) {
|
|
156
|
+
return this.callWith(method, params, IPC_METHODS[method].result, options);
|
|
157
|
+
}
|
|
158
|
+
/** The staged conversation contract. Use only with a server that opted into it, until cut-over. */
|
|
159
|
+
callConversation(method, params, options = {}) {
|
|
160
|
+
return this.callWith(method, params, CONVERSATION_METHODS[method].result, options);
|
|
161
|
+
}
|
|
162
|
+
callWith(method, params, result, options) {
|
|
156
163
|
if (this.isClosed)
|
|
157
164
|
return Promise.reject(new MessagingError("daemon_unavailable", "connection to the Messaging daemon is closed"));
|
|
158
165
|
if (options.signal?.aborted)
|
|
@@ -168,6 +175,7 @@ export class MessagingClient {
|
|
|
168
175
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
169
176
|
this.pending.set(id, {
|
|
170
177
|
method,
|
|
178
|
+
result,
|
|
171
179
|
resolve: resolve,
|
|
172
180
|
reject,
|
|
173
181
|
cleanup: () => {
|
|
@@ -236,7 +244,7 @@ export class MessagingClient {
|
|
|
236
244
|
this.settle(message.id, undefined, toError(message.error));
|
|
237
245
|
return;
|
|
238
246
|
}
|
|
239
|
-
const checked =
|
|
247
|
+
const checked = pending.result.safeParse(message.result);
|
|
240
248
|
if (!checked.success) {
|
|
241
249
|
this.settle(message.id, undefined, new MessagingError("internal", `daemon returned an invalid result for ${pending.method}`));
|
|
242
250
|
return;
|
|
@@ -2,7 +2,7 @@ import { mkdirSync } from "node:fs";
|
|
|
2
2
|
import { chmod, chown, rm, stat } from "node:fs/promises";
|
|
3
3
|
import net from "node:net";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
|
-
import { AGENT_METHODS, IPC_METHODS, IPC_PROTOCOL, IpcEvent, VERSION, LIMITS, MESSAGING_ERROR_RPC_CODE, MessagingError, parseWire, } from "../core/index.js";
|
|
5
|
+
import { AGENT_METHODS, CONVERSATION_METHODS, CONVERSATION_TIERS, IPC_METHODS, IPC_PROTOCOL, IpcEvent, VERSION, LIMITS, MESSAGING_ERROR_RPC_CODE, MessagingError, parseWire, } from "../core/index.js";
|
|
6
6
|
import { encodeLine, LineDecoder, LineTooLongError } from "./framing.js";
|
|
7
7
|
import { IS_WINDOWS, MAX_SOCKET_PATH_BYTES } from "./paths.js";
|
|
8
8
|
import { newPipeName, readPointer, writePointer } from "./pipes.js";
|
|
@@ -29,6 +29,13 @@ function agentTierRefusal(method) {
|
|
|
29
29
|
* `group` is refused there: Node cannot give a pipe to a group.
|
|
30
30
|
*/
|
|
31
31
|
export async function createIpcServer(options) {
|
|
32
|
+
return createServer(options, false);
|
|
33
|
+
}
|
|
34
|
+
/** Used only by the test entry: never changes the real daemon's method table. */
|
|
35
|
+
export async function createConversationIpcServerForTesting(options) {
|
|
36
|
+
return createServer({ ...options, handlers: options.handlers }, true);
|
|
37
|
+
}
|
|
38
|
+
async function createServer(options, conversations) {
|
|
32
39
|
const path = options.socketPath;
|
|
33
40
|
if (options.tier !== "agent" && options.tier !== "admin")
|
|
34
41
|
throw new MessagingError("invalid_request", "an IPC server needs a tier: agent or admin");
|
|
@@ -100,14 +107,16 @@ export async function createIpcServer(options) {
|
|
|
100
107
|
const { id, method } = request;
|
|
101
108
|
if (id === undefined)
|
|
102
109
|
return; // client notifications are not part of the API
|
|
103
|
-
const
|
|
110
|
+
const methods = conversations ? CONVERSATION_METHODS : IPC_METHODS;
|
|
111
|
+
const spec = Object.hasOwn(methods, method) ? methods[method] : undefined;
|
|
104
112
|
const handler = options.handlers[method];
|
|
105
|
-
if (!spec || (!handler && method !== "events.subscribe")) {
|
|
113
|
+
if (!spec || (!handler && (conversations || method !== "events.subscribe"))) {
|
|
106
114
|
ctx.send({ jsonrpc: "2.0", id, error: { code: RPC.methodNotFound, message: `method not found: ${method}` } });
|
|
107
115
|
return;
|
|
108
116
|
}
|
|
109
117
|
// The tier is checked before the params are read, so the agent socket tells nothing about the rest.
|
|
110
|
-
|
|
118
|
+
const agentAllowed = conversations ? CONVERSATION_TIERS[method] === "agent" : AGENT_TIER.has(method);
|
|
119
|
+
if (options.tier === "agent" && !agentAllowed) {
|
|
111
120
|
ctx.send({ jsonrpc: "2.0", id, error: toRpcError(new MessagingError("forbidden", agentTierRefusal(method))) });
|
|
112
121
|
return;
|
|
113
122
|
}
|
|
@@ -130,7 +139,7 @@ export async function createIpcServer(options) {
|
|
|
130
139
|
try {
|
|
131
140
|
const result = handler ? await handler(parsed.data, ctx, authority) : { ok: true };
|
|
132
141
|
// Subscriptions start only once the handler has accepted the keys.
|
|
133
|
-
if (method === "events.subscribe")
|
|
142
|
+
if (!conversations && method === "events.subscribe")
|
|
134
143
|
for (const key of parsed.data.keys)
|
|
135
144
|
ctx.subscriptions.add(key);
|
|
136
145
|
const checked = spec.result.safeParse(result);
|