@joe-broadhead/messaging 0.8.0 → 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 +150 -0
- package/dist/bin/messaging-mcp.js +202 -31
- package/dist/bin/messaging-relay.js +291 -43
- package/dist/bin/messaging.js +290 -32
- package/dist/bin/messagingd.js +1158 -138
- package/dist/lib/modules/core/agent/present.js +5 -0
- package/dist/lib/modules/core/agent-tools.js +12 -0
- package/dist/lib/modules/core/consent.js +54 -14
- package/dist/lib/modules/core/contact-card.js +3 -3
- package/dist/lib/modules/core/index.js +1 -1
- package/dist/lib/modules/core/jelly-host.js +3 -0
- package/dist/lib/modules/core/schema/common.js +2 -0
- package/dist/lib/modules/core/schema/consent.js +20 -3
- package/dist/lib/modules/core/schema/conv-frames.js +10 -3
- package/dist/lib/modules/core/schema/directory-frames.js +8 -3
- package/dist/lib/modules/core/schema/errors.js +3 -2
- package/dist/lib/modules/core/schema/ipc/consent.js +85 -10
- package/dist/lib/modules/core/schema/ipc/conversation-views.js +8 -1
- package/dist/lib/modules/core/schema/ipc/conversations.js +5 -1
- package/dist/lib/modules/core/schema/ipc/device.js +7 -0
- package/dist/lib/modules/core/schema/ipc/jelly.js +14 -7
- package/dist/lib/modules/core/schema/ipc/shapes.js +60 -1
- package/dist/lib/modules/core/schema/ipc.js +51 -3
- package/dist/lib/modules/core/version.js +1 -1
- package/dist/spec/messaging-v1.md +321 -49
- package/dist/types/core/agent/present.d.ts +1 -1
- package/dist/types/core/agent-tools.d.ts +32 -0
- package/dist/types/core/consent.d.ts +30 -7
- package/dist/types/core/contact-card.d.ts +3 -3
- package/dist/types/core/conversation-agent-tools.d.ts +21 -0
- package/dist/types/core/index.d.ts +1 -1
- package/dist/types/core/schema/common.d.ts +2 -0
- package/dist/types/core/schema/consent.d.ts +23 -2
- package/dist/types/core/schema/conv-frames.d.ts +6 -2
- package/dist/types/core/schema/directory-frames.d.ts +8 -3
- package/dist/types/core/schema/envelope.d.ts +2 -0
- package/dist/types/core/schema/errors.d.ts +2 -0
- package/dist/types/core/schema/ipc/consent.d.ts +377 -15
- package/dist/types/core/schema/ipc/conversation-views.d.ts +176 -0
- package/dist/types/core/schema/ipc/conversations.d.ts +368 -0
- package/dist/types/core/schema/ipc/device.d.ts +6 -0
- package/dist/types/core/schema/ipc/jelly.d.ts +14 -6
- package/dist/types/core/schema/ipc/shapes.d.ts +198 -0
- package/dist/types/core/schema/ipc/surfaces.d.ts +1 -0
- package/dist/types/core/schema/ipc/tasks.d.ts +2 -0
- package/dist/types/core/schema/ipc.d.ts +826 -3
- package/dist/types/core/schema/relay.d.ts +4 -0
- package/dist/types/core/version.d.ts +1 -1
- package/dist/types/ipc/server.d.ts +6 -0
- package/package.json +1 -1
|
@@ -14,8 +14,9 @@ import { Jws } from "../jelly.js";
|
|
|
14
14
|
import { DeviceKind } from "../directory.js";
|
|
15
15
|
import { DeviceChangeView, DeviceRemoveParams } from "./device.js";
|
|
16
16
|
/**
|
|
17
|
-
* A notice's id, stable per source: `m_<line id>` (a line), `
|
|
18
|
-
* A replay repeats the same ids, so
|
|
17
|
+
* A notice's id, stable per source: `m_<line id>` (a line), `m_task<task id>` (a task another person's agent asked of
|
|
18
|
+
* one of this person's agents), `r_<request id>` (a held request), `a_<id>` (an alert). A replay repeats the same ids, so
|
|
19
|
+
* a host rings once.
|
|
19
20
|
*/
|
|
20
21
|
export const NoticeId = z.string().regex(/^[mra]_[0-9A-Za-z]{1,64}$/, "notice id");
|
|
21
22
|
/** Most characters of a notice's `line`. */
|
|
@@ -34,9 +35,11 @@ export const NoticeDevice = DeviceChangeView.extend({
|
|
|
34
35
|
remove: z.enum(["token", "sign-in", "team"]).optional(),
|
|
35
36
|
});
|
|
36
37
|
/**
|
|
37
|
-
* One notice (design §13.11). `message`: a line in a DM or a channel from another person or their agent,
|
|
38
|
-
* agent's line to its own person
|
|
39
|
-
*
|
|
38
|
+
* One notice (design §13.11). `message`: a line in a DM or a channel from another person or their agent, an
|
|
39
|
+
* agent's line to its own person, or a task another person's agent asked of one of this person's agents (with `key`
|
|
40
|
+
* `task:<task id>`, and `from` that person), never a request: it waits on the agent, not the person; `request`: a request
|
|
41
|
+
* held for the person (an ask held for consent stays one); `alert`: an alarm of this device, a change of the person's
|
|
42
|
+
* other devices (with `device`), or trust the person gave that ends within 7 days (with `from`).
|
|
40
43
|
* `line` is the first line of a message, through `cleanPeerText`, only when the caller asked for lines; alerts carry
|
|
41
44
|
* fixed text. `from.name` and `from.email` are untrusted text in a hosted realm.
|
|
42
45
|
*/
|
|
@@ -46,6 +49,8 @@ export const Notice = z.object({
|
|
|
46
49
|
from: z.object({ name: z.string().max(200), email: z.string().max(320).optional(), org: z.enum(["same", "other", "none"]) }).optional(),
|
|
47
50
|
line: z.string().max(NOTICE_LINE_CHARS).optional(),
|
|
48
51
|
device: NoticeDevice.optional(),
|
|
52
|
+
/** Where a host opens it in its section: `task:<task id>` for a task notice; absent for the others. */
|
|
53
|
+
key: z.string().regex(/^task:[0-9A-HJKMNP-TV-Z]{26}$/, "task:<task id>").optional(),
|
|
49
54
|
at: Millis,
|
|
50
55
|
});
|
|
51
56
|
/** What the person's badges count: requests held for them. Conversation read state remains staged. */
|
|
@@ -102,7 +107,9 @@ export const JELLY_METHODS = {
|
|
|
102
107
|
* whose own person's mail may wake the host, and the tasks those sessions await (their unfinished requests). The relay
|
|
103
108
|
* presents the ticket once, for such traffic only, while this device is offline (spec §7, "Wake tickets"). `relay`:
|
|
104
109
|
* `armed` once the relay holds it, `pending` while it is not connected (it is sent at the next connection);
|
|
105
|
-
* `awaiting`: how many task requests the relay may wake for
|
|
110
|
+
* `awaiting`: how many task requests the relay may wake for; `dms`: how many dms the relay may wake for, those whose
|
|
111
|
+
* other person shares a channel with this person and is not blocked or muted (spec §8.11, "Wake tickets"). A
|
|
112
|
+
* directory realm only.
|
|
106
113
|
*/
|
|
107
114
|
"wake.arm": {
|
|
108
115
|
params: z.strictObject({
|
|
@@ -110,7 +117,7 @@ export const JELLY_METHODS = {
|
|
|
110
117
|
ticket: Jws.refine((ticket) => ticket.length <= WAKE_TICKET_BYTES, { message: `at most ${WAKE_TICKET_BYTES} bytes` }),
|
|
111
118
|
agents: z.array(SessionName).max(WAKE_AGENTS),
|
|
112
119
|
}),
|
|
113
|
-
result: z.object({ relay: z.enum(["armed", "pending"]), awaiting: z.number().int().nonnegative() }),
|
|
120
|
+
result: z.object({ relay: z.enum(["armed", "pending"]), awaiting: z.number().int().nonnegative(), dms: z.number().int().nonnegative() }),
|
|
114
121
|
},
|
|
115
122
|
/** Admin tier; host scope in `deployment: "jelly"`. The relay forgets this device's wake ticket (`pending` until connected). */
|
|
116
123
|
"wake.disarm": { params: z.strictObject({}), result: z.object({ relay: z.enum(["disarmed", "pending"]) }) },
|
|
@@ -12,6 +12,12 @@ import { DelegationSummary } from "../delegation.js";
|
|
|
12
12
|
import { ErrorCode } from "../errors.js";
|
|
13
13
|
/** Opaque, client-chosen session key, stable for the life of a client session, e.g. "opencode:<sessionID>". */
|
|
14
14
|
export const SessionKey = z.string().min(1).max(200);
|
|
15
|
+
/**
|
|
16
|
+
* A source-issued action token (spec §8.2, "Action tokens"): 32 random bytes, base64url. The daemon mints one per
|
|
17
|
+
* action, item and human-tier view, keeps it in memory only, and spends it at its first presentation. Opaque to
|
|
18
|
+
* clients; never logged.
|
|
19
|
+
*/
|
|
20
|
+
export const ActionToken = z.string().regex(/^[A-Za-z0-9_-]{43}$/, "action token");
|
|
15
21
|
/**
|
|
16
22
|
* When the recipient will likely see a message (spec §8.2): a hint from its machine's last presence
|
|
17
23
|
* at send time, never a promise.
|
|
@@ -94,7 +100,18 @@ export const TaskFollowUp = z.object({
|
|
|
94
100
|
/** When it reached `stage`. */
|
|
95
101
|
at: Millis,
|
|
96
102
|
});
|
|
97
|
-
|
|
103
|
+
/** Most characters of `TaskRecord.signIn.tool`: the executor's words, cleaned and capped like a preview. */
|
|
104
|
+
export const TASK_SIGN_IN_TOOL_CHARS = LIMITS.previewChars;
|
|
105
|
+
/** Most characters of `TaskRecord.refusal.reason`: the executor's words, cleaned and capped like a message request. */
|
|
106
|
+
export const TASK_REFUSAL_CHARS = LIMITS.requestChars;
|
|
107
|
+
/** An id a host's engine chose for its session or its pending approval or question: an opaque, plain identifier. */
|
|
108
|
+
export const HostLocalId = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/, "a plain identifier of at most 200 characters");
|
|
109
|
+
/**
|
|
110
|
+
* The executor host's own pending approval or question that an `AUTH_REQUIRED` or `INPUT_REQUIRED` task waits on
|
|
111
|
+
* (spec §8.2, "The task view"): set by the person's own client with `task.question`, held in this daemon's memory only.
|
|
112
|
+
*/
|
|
113
|
+
export const TaskQuestion = z.strictObject({ sessionId: HostLocalId, approvalId: HostLocalId });
|
|
114
|
+
const TaskRecordFields = z.object({
|
|
98
115
|
task: Task.extend({ id: Ulid }),
|
|
99
116
|
role: z.enum(["requester", "executor"]),
|
|
100
117
|
key: SessionKey.describe("local session that owns this side of the task"),
|
|
@@ -109,7 +126,49 @@ export const TaskRecord = z.object({
|
|
|
109
126
|
* Absent when there are none.
|
|
110
127
|
*/
|
|
111
128
|
followUps: z.array(TaskFollowUp).min(1).max(LIMITS.taskFollowUps).optional(),
|
|
129
|
+
// ---- the task view (spec §8.2, "The task view"): computed by this daemon as it projects the record; never stored or sent ----
|
|
130
|
+
/** The counterpart agent: the session the verified envelopes name (`peer.agent`). */
|
|
131
|
+
agent: SessionName.optional(),
|
|
132
|
+
/**
|
|
133
|
+
* The counterpart agent's person: `peer.person` and the display name the realm verified for it (the person id when
|
|
134
|
+
* the realm cannot name them). Never parsed from an agent's name. Absent while this daemon has not named them yet.
|
|
135
|
+
*/
|
|
136
|
+
person: z.object({ id: PersonId, name: z.string().max(200) }).optional(),
|
|
137
|
+
/** That person's relation to this device's person, as `ParticipantView.org` computes it; absent with `person`. */
|
|
138
|
+
org: z.enum(["same", "other", "none"]).optional(),
|
|
139
|
+
/**
|
|
140
|
+
* `AUTH_REQUIRED` only. `by`: `you` when the executor is an agent of this device's person, else `them`, computed here,
|
|
141
|
+
* never read from the peer. `tool`: the tool the executor named (`metadata[URI.signIn]` of its status message), as
|
|
142
|
+
* plain text cleaned and capped at `TASK_SIGN_IN_TOOL_CHARS`; absent when it named none.
|
|
143
|
+
*/
|
|
144
|
+
signIn: z.object({ tool: z.string().min(1).max(TASK_SIGN_IN_TOOL_CHARS).optional(), by: z.enum(["you", "them"]) }).optional(),
|
|
145
|
+
/** `REJECTED` only, when the executor gave words: its status message's text, cleaned and capped at `TASK_REFUSAL_CHARS`. */
|
|
146
|
+
refusal: z.object({ reason: z.string().min(1).max(TASK_REFUSAL_CHARS) }).optional(),
|
|
147
|
+
/**
|
|
148
|
+
* The executor's side, on the admin socket only (human tier), in `AUTH_REQUIRED` or `INPUT_REQUIRED`: the host's own
|
|
149
|
+
* approval or question the task waits on (`task.question`). Never on the agent tier, never sent to the peer.
|
|
150
|
+
*/
|
|
151
|
+
question: TaskQuestion.optional(),
|
|
112
152
|
});
|
|
153
|
+
/** The task view's fields go with the states they belong to (spec §8.2, "The task view"). */
|
|
154
|
+
function taskViewRules(schema) {
|
|
155
|
+
return schema
|
|
156
|
+
.refine((r) => r.signIn === undefined || r.task.status.state === "TASK_STATE_AUTH_REQUIRED", "signIn is for an AUTH_REQUIRED task")
|
|
157
|
+
.refine((r) => r.refusal === undefined || r.task.status.state === "TASK_STATE_REJECTED", "refusal is for a REJECTED task")
|
|
158
|
+
.refine((r) => r.question === undefined || (r.role === "executor" && (r.task.status.state === "TASK_STATE_AUTH_REQUIRED" || r.task.status.state === "TASK_STATE_INPUT_REQUIRED")), "question is for the executor's side of an AUTH_REQUIRED or INPUT_REQUIRED task");
|
|
159
|
+
}
|
|
160
|
+
export const TaskRecord = taskViewRules(TaskRecordFields);
|
|
161
|
+
/**
|
|
162
|
+
* One side of a task one of this person's agents on this device is in, as the person's own clients see it (spec §8.2,
|
|
163
|
+
* "The person's tasks"): the task view without the session key (a bearer credential), with `session` the agent's name
|
|
164
|
+
* and, on the requester side of a task that has not ended, J2's `cancel` action minted for this view.
|
|
165
|
+
*/
|
|
166
|
+
export const PersonTask = taskViewRules(TaskRecordFields.omit({ key: true }).extend({
|
|
167
|
+
/** Never present: a session key is a bearer credential for its session. */
|
|
168
|
+
key: z.never().optional(),
|
|
169
|
+
session: SessionName.describe("this person's agent on this device that holds this side of the task"),
|
|
170
|
+
cancel: z.strictObject({ name: z.literal("cancel"), token: ActionToken }).optional(),
|
|
171
|
+
})).refine((r) => r.cancel === undefined || r.role === "requester", "cancel is for the requester side");
|
|
113
172
|
/**
|
|
114
173
|
* Where a retained task envelope went: an agent session `person/agent@label`.
|
|
115
174
|
*/
|
|
@@ -23,7 +23,7 @@ import { DEVICE_METHODS, DeviceStatus, LoginPurpose } from "./ipc/device.js";
|
|
|
23
23
|
import { Artifact, Message, Part, TaskState, TaskStatus } from "./a2a.js";
|
|
24
24
|
import { AgentKind, DeliveryStage, DeliveryTier, EncKey, Hash, MachineId, MachineLabel, MemberRole, Millis, PersonId, PersonName, RelayUrl, SessionAddress, SessionName, SessionState, TeamId, TeamName, Ulid, LIMITS, } from "./common.js";
|
|
25
25
|
import { ErrorCode } from "./errors.js";
|
|
26
|
-
import { InboxItem, SessionKey, TaskRecord } from "./ipc/shapes.js";
|
|
26
|
+
import { ActionToken, InboxItem, PersonTask, SessionKey, TASK_SIGN_IN_TOOL_CHARS, TaskQuestion, TaskRecord } from "./ipc/shapes.js";
|
|
27
27
|
import { SURFACE_METHODS } from "./ipc/surfaces.js";
|
|
28
28
|
import { Member } from "./roster.js";
|
|
29
29
|
import { CONSENT_METHODS } from "./ipc/consent.js";
|
|
@@ -206,11 +206,53 @@ export const IPC_METHODS = {
|
|
|
206
206
|
artifact: Artifact.optional(),
|
|
207
207
|
append: z.boolean().optional(),
|
|
208
208
|
lastChunk: z.boolean().optional(),
|
|
209
|
-
|
|
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"),
|
|
210
212
|
result: TaskRecord,
|
|
211
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
|
+
*/
|
|
212
252
|
"task.cancel": {
|
|
213
|
-
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"),
|
|
214
256
|
result: TaskRecord,
|
|
215
257
|
},
|
|
216
258
|
"events.subscribe": { params: z.strictObject({ keys: z.array(SessionKey).min(1).max(256) }), result: Ok },
|
|
@@ -327,6 +369,7 @@ export const AGENT_METHODS = [
|
|
|
327
369
|
"task.get",
|
|
328
370
|
"task.update",
|
|
329
371
|
"task.cancel",
|
|
372
|
+
"task.question",
|
|
330
373
|
];
|
|
331
374
|
/**
|
|
332
375
|
* The human tier (design §7.5): methods that act as the person, on `run/admin.sock` only: their
|
|
@@ -340,6 +383,9 @@ export const HUMAN_METHODS = [
|
|
|
340
383
|
"grant.list",
|
|
341
384
|
"grant.set",
|
|
342
385
|
"grant.set.preview",
|
|
386
|
+
"reach.get",
|
|
387
|
+
"reach.set",
|
|
388
|
+
"reach.set.preview",
|
|
343
389
|
"request.list",
|
|
344
390
|
"request.answer",
|
|
345
391
|
"request.answer.preview",
|
|
@@ -349,6 +395,8 @@ export const HUMAN_METHODS = [
|
|
|
349
395
|
"notice.subscribe",
|
|
350
396
|
"notice.list",
|
|
351
397
|
"notice.ack",
|
|
398
|
+
"tasks.list",
|
|
399
|
+
"tasks.get",
|
|
352
400
|
"device.approve.preview",
|
|
353
401
|
"device.remove.preview",
|
|
354
402
|
"identity.replace.preview",
|