@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/dist/bin/messaging-mcp.js +202 -31
  3. package/dist/bin/messaging-relay.js +291 -43
  4. package/dist/bin/messaging.js +290 -32
  5. package/dist/bin/messagingd.js +1158 -138
  6. package/dist/lib/modules/core/agent/present.js +5 -0
  7. package/dist/lib/modules/core/agent-tools.js +12 -0
  8. package/dist/lib/modules/core/consent.js +54 -14
  9. package/dist/lib/modules/core/contact-card.js +3 -3
  10. package/dist/lib/modules/core/index.js +1 -1
  11. package/dist/lib/modules/core/jelly-host.js +3 -0
  12. package/dist/lib/modules/core/schema/common.js +2 -0
  13. package/dist/lib/modules/core/schema/consent.js +20 -3
  14. package/dist/lib/modules/core/schema/conv-frames.js +10 -3
  15. package/dist/lib/modules/core/schema/directory-frames.js +8 -3
  16. package/dist/lib/modules/core/schema/errors.js +3 -2
  17. package/dist/lib/modules/core/schema/ipc/consent.js +85 -10
  18. package/dist/lib/modules/core/schema/ipc/conversation-views.js +8 -1
  19. package/dist/lib/modules/core/schema/ipc/conversations.js +5 -1
  20. package/dist/lib/modules/core/schema/ipc/device.js +7 -0
  21. package/dist/lib/modules/core/schema/ipc/jelly.js +14 -7
  22. package/dist/lib/modules/core/schema/ipc/shapes.js +60 -1
  23. package/dist/lib/modules/core/schema/ipc.js +51 -3
  24. package/dist/lib/modules/core/version.js +1 -1
  25. package/dist/spec/messaging-v1.md +321 -49
  26. package/dist/types/core/agent/present.d.ts +1 -1
  27. package/dist/types/core/agent-tools.d.ts +32 -0
  28. package/dist/types/core/consent.d.ts +30 -7
  29. package/dist/types/core/contact-card.d.ts +3 -3
  30. package/dist/types/core/conversation-agent-tools.d.ts +21 -0
  31. package/dist/types/core/index.d.ts +1 -1
  32. package/dist/types/core/schema/common.d.ts +2 -0
  33. package/dist/types/core/schema/consent.d.ts +23 -2
  34. package/dist/types/core/schema/conv-frames.d.ts +6 -2
  35. package/dist/types/core/schema/directory-frames.d.ts +8 -3
  36. package/dist/types/core/schema/envelope.d.ts +2 -0
  37. package/dist/types/core/schema/errors.d.ts +2 -0
  38. package/dist/types/core/schema/ipc/consent.d.ts +377 -15
  39. package/dist/types/core/schema/ipc/conversation-views.d.ts +176 -0
  40. package/dist/types/core/schema/ipc/conversations.d.ts +368 -0
  41. package/dist/types/core/schema/ipc/device.d.ts +6 -0
  42. package/dist/types/core/schema/ipc/jelly.d.ts +14 -6
  43. package/dist/types/core/schema/ipc/shapes.d.ts +198 -0
  44. package/dist/types/core/schema/ipc/surfaces.d.ts +1 -0
  45. package/dist/types/core/schema/ipc/tasks.d.ts +2 -0
  46. package/dist/types/core/schema/ipc.d.ts +826 -3
  47. package/dist/types/core/schema/relay.d.ts +4 -0
  48. package/dist/types/core/version.d.ts +1 -1
  49. package/dist/types/ipc/server.d.ts +6 -0
  50. 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), `r_<request id>` (a held request), `a_<id>` (an alert).
18
- * A replay repeats the same ids, so a host rings once.
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, or an
38
- * agent's line to its own person; `request`: a request held for the person; `alert`: an alarm of this device, a
39
- * change of the person's other devices (with `device`), or trust the person gave that ends within 7 days (with `from`).
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. A directory realm only.
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
- export const TaskRecord = z.object({
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.strictObject({ key: SessionKey, taskId: Ulid, reason: z.string().max(1000).optional() }),
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",
@@ -3,4 +3,4 @@
3
3
  * its IPC hello and a client compares it with its own (spec §8.2). A test keeps it equal to the
4
4
  * root package.json.
5
5
  */
6
- export const VERSION = "0.8.0";
6
+ export const VERSION = "0.9.0";