salt-agent-sdk 0.1.0 → 0.18.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 (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +941 -30
  3. package/dist/actions.d.ts +21 -5
  4. package/dist/actions.d.ts.map +1 -1
  5. package/dist/actions.js +673 -54
  6. package/dist/actions.js.map +1 -1
  7. package/dist/ask.d.ts +83 -0
  8. package/dist/ask.d.ts.map +1 -0
  9. package/dist/ask.js +215 -0
  10. package/dist/ask.js.map +1 -0
  11. package/dist/chatPrivacy.d.ts +34 -0
  12. package/dist/chatPrivacy.d.ts.map +1 -0
  13. package/dist/chatPrivacy.js +60 -0
  14. package/dist/chatPrivacy.js.map +1 -0
  15. package/dist/client.d.ts +2783 -35
  16. package/dist/client.d.ts.map +1 -1
  17. package/dist/client.js +1200 -143
  18. package/dist/client.js.map +1 -1
  19. package/dist/config.d.ts +16 -6
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +16 -5
  22. package/dist/config.js.map +1 -1
  23. package/dist/crypto.d.ts +62 -0
  24. package/dist/crypto.d.ts.map +1 -1
  25. package/dist/crypto.js +121 -0
  26. package/dist/crypto.js.map +1 -1
  27. package/dist/delegations.d.ts +25 -8
  28. package/dist/delegations.d.ts.map +1 -1
  29. package/dist/delegations.js +67 -11
  30. package/dist/delegations.js.map +1 -1
  31. package/dist/device/client.d.ts +161 -0
  32. package/dist/device/client.d.ts.map +1 -0
  33. package/dist/device/client.js +601 -0
  34. package/dist/device/client.js.map +1 -0
  35. package/dist/device/host.d.ts +222 -0
  36. package/dist/device/host.d.ts.map +1 -0
  37. package/dist/device/host.js +891 -0
  38. package/dist/device/host.js.map +1 -0
  39. package/dist/device/http.d.ts +12 -0
  40. package/dist/device/http.d.ts.map +1 -0
  41. package/dist/device/http.js +124 -0
  42. package/dist/device/http.js.map +1 -0
  43. package/dist/device/index.d.ts +5 -0
  44. package/dist/device/index.d.ts.map +1 -0
  45. package/dist/device/index.js +27 -0
  46. package/dist/device/index.js.map +1 -0
  47. package/dist/device/protocol.d.ts +312 -0
  48. package/dist/device/protocol.d.ts.map +1 -0
  49. package/dist/device/protocol.js +435 -0
  50. package/dist/device/protocol.js.map +1 -0
  51. package/dist/identities.d.ts +9 -2
  52. package/dist/identities.d.ts.map +1 -1
  53. package/dist/identities.js +33 -3
  54. package/dist/identities.js.map +1 -1
  55. package/dist/identity.d.ts +155 -0
  56. package/dist/identity.d.ts.map +1 -0
  57. package/dist/identity.js +196 -0
  58. package/dist/identity.js.map +1 -0
  59. package/dist/identityShare.d.ts +129 -0
  60. package/dist/identityShare.d.ts.map +1 -0
  61. package/dist/identityShare.js +308 -0
  62. package/dist/identityShare.js.map +1 -0
  63. package/dist/ids.d.ts +11 -0
  64. package/dist/ids.d.ts.map +1 -0
  65. package/dist/ids.js +17 -0
  66. package/dist/ids.js.map +1 -0
  67. package/dist/index.d.ts +13 -0
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +13 -0
  70. package/dist/index.js.map +1 -1
  71. package/dist/reconcile.d.ts +25 -0
  72. package/dist/reconcile.d.ts.map +1 -0
  73. package/dist/reconcile.js +60 -0
  74. package/dist/reconcile.js.map +1 -0
  75. package/dist/recovery.d.ts +51 -0
  76. package/dist/recovery.d.ts.map +1 -0
  77. package/dist/recovery.js +59 -0
  78. package/dist/recovery.js.map +1 -0
  79. package/dist/register.d.ts +32 -0
  80. package/dist/register.d.ts.map +1 -0
  81. package/dist/register.js +65 -0
  82. package/dist/register.js.map +1 -0
  83. package/dist/scriptJob.d.ts +92 -0
  84. package/dist/scriptJob.d.ts.map +1 -0
  85. package/dist/scriptJob.js +270 -0
  86. package/dist/scriptJob.js.map +1 -0
  87. package/dist/sessions.d.ts +131 -0
  88. package/dist/sessions.d.ts.map +1 -0
  89. package/dist/sessions.js +240 -0
  90. package/dist/sessions.js.map +1 -0
  91. package/dist/socket.d.ts +115 -0
  92. package/dist/socket.d.ts.map +1 -0
  93. package/dist/socket.js +1038 -0
  94. package/dist/socket.js.map +1 -0
  95. package/dist/webhook.d.ts +613 -24
  96. package/dist/webhook.d.ts.map +1 -1
  97. package/dist/webhook.js +1384 -83
  98. package/dist/webhook.js.map +1 -1
  99. package/dist/work.d.ts +57 -0
  100. package/dist/work.d.ts.map +1 -0
  101. package/dist/work.js +213 -0
  102. package/dist/work.js.map +1 -0
  103. package/package.json +26 -4
package/dist/webhook.d.ts CHANGED
@@ -1,20 +1,51 @@
1
1
  import { type Express } from "express";
2
- import type { SaltClient } from "./client";
2
+ import type { AskOptions, AskResult } from "./ask.js";
3
+ import type { Mandate, MandateExercise, ReactionSummary, SaltClient } from "./client";
4
+ import * as identityShare from "./identityShare.js";
5
+ import type { IdentityAskHandler, IdentitySharedHandler } from "./identityShare.js";
3
6
  import type { AgentIdentity, IdentityStore } from "./identities";
7
+ import { type SaltId } from "./ids.js";
8
+ import type { Session, SessionStore } from "./sessions";
9
+ /** See MessageContext.deliveredBecause / SessionTurn.deliveredBecause. */
10
+ export type DeliveredBecause = "mention" | "reply" | "keyword" | "all";
11
+ /** The original post a reply answers, exactly as salt-api sent it on the delivery
12
+ * (`message.parent`) -- present only for a profile-room reply, where the post
13
+ * is public plaintext. Never built locally. See MessageContext.parent. */
14
+ export interface ReplyParent {
15
+ id: SaltId;
16
+ author: unknown;
17
+ excerpt: string;
18
+ }
4
19
  /** The exact marker a hand-off briefing must start with, so the incoming
5
20
  * agent's polling loop (below) can detect it arrived. Consumers building
6
21
  * the outgoing briefing prompt must instruct their model to emit this
7
22
  * exact line first. */
8
23
  export declare const HANDOFF_BRIEFING_MARKER = "[[SALT-HANDOFF-BRIEFING]]";
9
24
  export interface RawChatMeta {
10
- active_agent_id?: number | string;
11
- mediator_agent_id?: number | string;
12
- coaching_for_chat_id?: number | string;
25
+ active_agent_id?: SaltId;
26
+ mediator_agent_id?: SaltId;
27
+ coaching_for_chat_id?: SaltId;
28
+ /** True when this coaching chat is a PRIVATE advisor lane rather than the
29
+ * mutual Mediator lane. A private advisor is not a member of the shared
30
+ * chat and therefore cannot decrypt it -- this flag stops us asking. */
31
+ private_lane?: boolean;
32
+ /** The kind of lane this chat is, when it is one. "consult" for a lane consult_agent opened -- see webhook.ts's CONSULT_RUNAWAY_LIMIT and actions.ts's request_floor. Absent (or any other value) for an ordinary chat or another lane kind (Mediator coaching, a translator, a work.ts report lane). */
33
+ lane_kind?: string;
13
34
  mode?: "auto" | "manual";
14
35
  [key: string]: unknown;
15
36
  }
37
+ /** What `onDeviceLaneMessage` receives. */
38
+ export interface DeviceLaneMessage {
39
+ identity: AgentIdentity;
40
+ chatId: SaltId;
41
+ senderId: SaltId;
42
+ sender: RawSender;
43
+ /** The decrypted lane message (a device-protocol line). */
44
+ text: string;
45
+ chatMeta?: RawChatMeta;
46
+ }
16
47
  export interface RawSender {
17
- id: number | string;
48
+ id: SaltId;
18
49
  display_name?: string;
19
50
  account_type?: "User" | "Agent";
20
51
  [key: string]: unknown;
@@ -23,44 +54,170 @@ export interface DecryptedAttachment {
23
54
  filename: string;
24
55
  contentType: string;
25
56
  size: number;
26
- /** Present only when contentType starts with "image/" -- the SDK doesn't
27
- * know how to represent other file types beyond a note in `unsupportedNote`. */
57
+ /** The decrypted file bytes -- present for every content type once the
58
+ * declared size is within pgp.MAX_ATTACHMENT_BYTES (images, PDFs, and
59
+ * anything else); absent only when `unsupportedNote` explains why
60
+ * (oversized, or the decrypt/download itself failed). */
28
61
  data?: Buffer;
62
+ /** UTF-8 decoded `data`, set only for text-shaped content: text/*, JSON,
63
+ * CSV, or Markdown (see webhook.ts's isTextLikeAttachment). Not set for
64
+ * images, audio/video, or PDF -- a PDF's bytes are handed through as
65
+ * `data` for a consumer to extract text from itself (e.g. Python's
66
+ * pypdf); the SDK has no pure-JS PDF parser in its dependency tree to
67
+ * do that extraction here. */
68
+ text?: string;
29
69
  unsupportedNote?: string;
30
70
  }
31
71
  export interface MessageContext {
32
72
  identity: AgentIdentity;
33
- chatId: number;
34
- senderId: number;
73
+ chatId: SaltId;
74
+ senderId: SaltId;
35
75
  sender: RawSender;
36
- /** Delegation-marker-stripped plaintext (see delegations.parseIncoming). */
76
+ /** Delegation- and consult-marker-stripped plaintext (see delegations.parseIncoming / delegations.stripConsultMarker). */
37
77
  text: string;
78
+ /** False for an open-room message (salt-api's `message.encrypted === false`
79
+ * on the delivery) -- `text` came straight off the wire with no PGP
80
+ * decrypt attempted, so a reply into this chat should go through
81
+ * `client.postPlainMessage`, never `reply()`'s normal encrypt-for-every-
82
+ * member path. True (the default) for an ordinary end-to-end encrypted
83
+ * chat. See client.ts's postPlainMessage/setChatSubscription. */
84
+ encrypted: boolean;
85
+ /** Why THIS delivery reached this identity, on an open room only --
86
+ * salt-api's `message.delivered_because` (present only alongside
87
+ * `encrypted: false`; undefined for an ordinary encrypted chat, where
88
+ * every member always gets every message and the question doesn't
89
+ * apply). `"mention"`/`"reply"` are the two ways an `"addressed"`
90
+ * subscription (client.ts's setChatSubscription) can trigger; `"keyword"`
91
+ * is one of that subscription's own `keywords` appearing in the message;
92
+ * `"all"` means the subscription just wants everything. */
93
+ deliveredBecause?: DeliveredBecause;
94
+ /** The id of the message this one replies to (salt-api's
95
+ * `message.reply_to_message_id`); undefined for a top-level message. */
96
+ replyTo?: SaltId;
97
+ /** The original post this message replies to, when salt-api sent it
98
+ * (`message.parent`: profile-room replies only). Undefined otherwise --
99
+ * an agent should treat absence as "origin not provided", not "none". */
100
+ parent?: ReplyParent;
38
101
  /** Delegation hop depth this message arrived at; pass through to any further delegate call. */
39
102
  delegationDepth: number;
40
103
  chatMeta?: RawChatMeta;
104
+ /** The shared chat this message's conversation ultimately serves --
105
+ * `chatId` itself for an ordinary chat, or `chatMeta.coaching_for_chat_id`
106
+ * when `chatId` is a lane (a sidechain, Mediator coaching, a consult).
107
+ * Lets a handler act on "the room" even while replying inside a lane
108
+ * (e.g. request_floor's hand-off target). */
109
+ roomId: SaltId;
41
110
  /** Set when this identity is the configured Mediator privately coaching
42
111
  * someone about a shared chat it also observes -- the generic decrypted
43
112
  * transcript of that shared chat (not yet wrapped in any prompt framing). */
44
113
  mediatorSharedContext?: string;
45
- /** Set when the inbound message was an Attachment with decryptable metadata. */
114
+ /** Set when the inbound message was an Attachment with decryptable
115
+ * metadata -- images, PDFs, and any other file type all carry decrypted
116
+ * `data` bytes now (bounded by pgp.MAX_ATTACHMENT_BYTES); text-shaped
117
+ * files (text/*, JSON, CSV, Markdown) also carry a decoded `text` field.
118
+ * See DecryptedAttachment. */
46
119
  attachment?: DecryptedAttachment;
120
+ /** This identity's memory of `chatId`: recent turns and a short note,
121
+ * loaded (or rebuilt from chat history, on a cold start) before this
122
+ * handler runs and persisted after it returns. A plain mutable object --
123
+ * write to `session.note` directly (e.g. session.note.goal, or push onto
124
+ * session.note.consulted) to have it survive a restart and ride along a
125
+ * hand-off. Ignore it entirely and nothing changes. */
126
+ session: Session;
47
127
  /** Encrypts `text` for every current chat member (+ this identity's own
48
128
  * copy), posts it, drains this reply's delegation trail onto it, and
49
129
  * emits an agent_reply_sent metric. Handles a typing-indicator heartbeat
50
130
  * for the duration of the call automatically. */
51
131
  reply(text: string): Promise<void>;
132
+ /** The id of the message being handled (the delivery's `message_id`). */
133
+ messageId: SaltId;
134
+ /** React to THIS message with one emoji; resolves with the server's reactions summary.
135
+ * Use sparingly -- the owner's rule: "not all the time, just when they choose", and only
136
+ * "if it relevantly complements the chat in a friendly way" (acknowledge thanks, mark a
137
+ * request done, celebrate good news). Never instead of answering a question, never on
138
+ * every message, never on your own, at most one per message. Calling it again with the
139
+ * same emoji REMOVES your reaction (the API is a toggle). A rejection (not a single
140
+ * emoji, over the 12 cap) throws a SaltApiError with the server's sentence. */
141
+ react(emoji: string): Promise<ReactionSummary>;
142
+ /** Asks a quick inline question (K3) -- a card with one button per
143
+ * option, plus (by default, when there are no options) an invitation to
144
+ * type a free-form reply -- and resolves with whichever answer arrives
145
+ * first. Works identically under webhook and socket mode. See ask.ts. */
146
+ ask(question: string, opts?: AskOptions): Promise<AskResult>;
147
+ /** Sugar for `ask` with Yes/No buttons. See ask.ts's approve(). */
148
+ approve(summary: string, opts?: {
149
+ timeoutMs?: number;
150
+ }): Promise<{
151
+ approved: boolean;
152
+ by: SaltId;
153
+ via: "button" | "message";
154
+ }>;
155
+ /** Shares `keys` from this identity's own card into `chatId` -- one signed SLICE to every non-observer member (see identityShare.ts's share()). */
156
+ shareIdentity(keys: string[], opts?: identityShare.ShareOptions): Promise<identityShare.ShareResult>;
157
+ }
158
+ /** An incoming `[[SALT-WORK-CANCEL id=...]]` (work.ts) -- the person
159
+ * cancelling a `scheduled` report, sent as a whole message into the same
160
+ * lane the report came from. Always intercepted before `onMessage`, same
161
+ * as a SALT-IDENTITY-SLICE/DECLINE/REVOKE (there's no "ordinary text"
162
+ * reading of a bare cancel marker). See WebhookServerOptions.onWorkCancel. */
163
+ export interface WorkCancelContext {
164
+ identity: AgentIdentity;
165
+ /** The work id named by the marker. */
166
+ workId: string;
167
+ /** The chat the cancelled work came from -- matches work.ts's
168
+ * WorkTarget.chatId (the room, not the lane the cancel arrived in). */
169
+ chatId: SaltId;
170
+ /** The lane chat this cancel message actually arrived in -- the same
171
+ * sidechain the report it's cancelling was posted into. Equal to
172
+ * `chatId` when, unusually, the cancel didn't arrive inside a lane. */
173
+ laneId: SaltId;
174
+ /** Whoever sent the cancel -- the person the work was for. */
175
+ sender: RawSender;
52
176
  }
53
177
  export interface CardInteractionContext {
54
178
  identity: AgentIdentity;
55
- chatId: number;
56
- cardId: number | string;
179
+ chatId: SaltId;
180
+ cardId: SaltId;
57
181
  actionId: string;
58
182
  user: RawSender;
59
183
  blocks: unknown;
184
+ /** Every `input` block's current value at tap time, keyed by `block_id`
185
+ * (NEW) -- `{}` when the card has no input blocks or the tapped button
186
+ * ignored them (pay/handoff buttons always do). See client.ts's
187
+ * InputCardBlock / CardInteraction.values. */
188
+ values: Record<string, string>;
189
+ /** This identity's memory of `chatId` (see MessageContext.session). Loaded
190
+ * before this handler runs, same as everywhere else -- but there's no
191
+ * reply() here (a card update IS the response), so nothing is appended
192
+ * or persisted afterward. Read it; write session.note if you want, but
193
+ * know it won't be saved from this call. */
194
+ session: Session;
195
+ }
196
+ export interface AppActionContext {
197
+ identity: AgentIdentity;
198
+ installationId: SaltId;
199
+ /** The app's identity and the PINNED version number this installation was running at ask time -- not necessarily the app's latest published version. */
200
+ app: {
201
+ id: SaltId;
202
+ name: string;
203
+ version: number;
204
+ };
205
+ /** Null for a personal ("Your apps") installation -- `reply()` then has nothing to post into and fails soft (logged), the same way `salt.post` on the bridge side rejects with "Not in a chat." */
206
+ chatId: SaltId | null;
207
+ /** The ask's own text (1..2000, salt-api-enforced). */
208
+ text: string;
209
+ /** The ask's optional structured payload (≤ 16 KB, salt-api-enforced). */
210
+ data?: Record<string, unknown>;
211
+ /** The shared state's `version` at ask time -- a snapshot, not a lock; read fresh with `client.apps.state` (or just `setState`'s own conflict) before writing. */
212
+ stateVersion: number;
213
+ /** Whoever tapped `salt.ask` inside the app. */
214
+ sender: RawSender;
215
+ /** Encrypts `text` for every current chat member (+ this identity's own copy) and posts it -- same closure every other context type gets. A no-op (logged) on a personal installation, which has no chat to post into. */
216
+ reply(text: string): Promise<void>;
60
217
  }
61
218
  export interface InvoicePaidContext {
62
219
  identity: AgentIdentity;
63
- chatId: number;
220
+ chatId: SaltId;
64
221
  buyer: RawSender;
65
222
  lineItems: Array<{
66
223
  name: string;
@@ -70,24 +227,290 @@ export interface InvoicePaidContext {
70
227
  amount: string | number;
71
228
  /** True for a "Credits top-up" invoice -- no delivery owed, the credit already landed server-side. */
72
229
  isTopUp: boolean;
73
- transferRequestId: number | string;
230
+ transferRequestId: SaltId;
231
+ /** This identity's memory of `chatId` (see MessageContext.session). Whatever you reply() with here is appended as an assistant turn and persisted, same as onMessage. */
232
+ session: Session;
74
233
  reply(text: string): Promise<void>;
234
+ /** See MessageContext.ask. */
235
+ ask(question: string, opts?: AskOptions): Promise<AskResult>;
236
+ /** See MessageContext.approve. */
237
+ approve(summary: string, opts?: {
238
+ timeoutMs?: number;
239
+ }): Promise<{
240
+ approved: boolean;
241
+ by: SaltId;
242
+ via: "button" | "message";
243
+ }>;
244
+ /** See MessageContext.shareIdentity. */
245
+ shareIdentity(keys: string[], opts?: identityShare.ShareOptions): Promise<identityShare.ShareResult>;
246
+ }
247
+ export interface ChatOpenedChat {
248
+ id: SaltId;
249
+ name?: string;
250
+ public?: boolean;
251
+ managed?: boolean;
252
+ open_invite?: boolean;
253
+ mode?: "auto" | "manual";
254
+ active_agent_id?: SaltId;
255
+ mediator_agent_id?: SaltId;
256
+ coaching_for_chat_id?: SaltId;
257
+ private_lane?: boolean;
258
+ /** False for an open room (no end-to-end encryption). Absent on an older
259
+ * server that predates open rooms, which always means an ordinary
260
+ * encrypted chat -- see ChatOpenedContext.encrypted for the resolved,
261
+ * defaulted value this SDK actually exposes. */
262
+ encrypted?: boolean;
263
+ [key: string]: unknown;
264
+ }
265
+ export interface ChatOpenedMember extends RawSender {
266
+ observer?: boolean;
267
+ }
268
+ export interface ChatOpenedContext {
269
+ identity: AgentIdentity;
270
+ chatId: SaltId;
271
+ chat: ChatOpenedChat;
272
+ /** False for an open room -- `chat.encrypted === false` on the payload,
273
+ * defaulted to true when the field is absent (an older server, or an
274
+ * ordinary encrypted chat, where the question doesn't apply). `reply()`
275
+ * below always PGP-encrypts regardless -- greet an open room plainly
276
+ * with `client.postPlainMessage(identity.apiKey, chatId, text)` instead,
277
+ * same convention as MessageContext.encrypted. */
278
+ encrypted: boolean;
279
+ /** The person (or agent -- see `openedBy.account_type`) who newly opened
280
+ * this chat: a fresh 1:1, a new group that includes this identity, or an
281
+ * add-to-an-existing-group. */
282
+ openedBy: RawSender;
283
+ /** Every current member, including this identity's own account. */
284
+ members: ChatOpenedMember[];
285
+ openedAt: string;
286
+ /** This identity's memory of `chatId` (see MessageContext.session). Whatever you reply() with here (e.g. a greeting) is appended as an assistant turn and persisted, same as onMessage. */
287
+ session: Session;
288
+ /** Encrypts `text` for every current chat member (+ this identity's own
289
+ * copy) and posts it -- the same closure every other context type gets. */
290
+ reply(text: string): Promise<void>;
291
+ /** See MessageContext.ask. */
292
+ ask(question: string, opts?: AskOptions): Promise<AskResult>;
293
+ /** See MessageContext.approve. */
294
+ approve(summary: string, opts?: {
295
+ timeoutMs?: number;
296
+ }): Promise<{
297
+ approved: boolean;
298
+ by: SaltId;
299
+ via: "button" | "message";
300
+ }>;
301
+ /** See MessageContext.shareIdentity. */
302
+ shareIdentity(keys: string[], opts?: identityShare.ShareOptions): Promise<identityShare.ShareResult>;
75
303
  }
76
304
  export interface HandoffConfirmedContext {
77
305
  identity: AgentIdentity;
78
- chatId: number;
306
+ chatId: SaltId;
79
307
  reason?: string;
308
+ /** Set only when this hand-off was triggered automatically by the
309
+ * consulted agent's request_floor call rather than a model-chosen
310
+ * hand_off_to_agent/hand_back_to_concierge: the consult lane's decrypted
311
+ * transcript so far (capped), for the briefing to draw on. */
312
+ consultTranscript?: string;
313
+ /** This identity's memory of `chatId` (see MessageContext.session): the
314
+ * tail and note it built up while it was the active agent here, so a
315
+ * briefing composed from it isn't starting cold. Whatever you reply()
316
+ * with (the briefing) is appended as an assistant turn and persisted --
317
+ * moot once you've handed off, but there if you're ever handed this
318
+ * chat again later. */
319
+ session: Session;
80
320
  reply(text: string): Promise<void>;
321
+ /** See MessageContext.ask. */
322
+ ask(question: string, opts?: AskOptions): Promise<AskResult>;
323
+ /** See MessageContext.approve. */
324
+ approve(summary: string, opts?: {
325
+ timeoutMs?: number;
326
+ }): Promise<{
327
+ approved: boolean;
328
+ by: SaltId;
329
+ via: "button" | "message";
330
+ }>;
331
+ /** See MessageContext.shareIdentity. */
332
+ shareIdentity(keys: string[], opts?: identityShare.ShareOptions): Promise<identityShare.ShareResult>;
81
333
  }
82
334
  export interface HandoffReceivedContext {
83
335
  identity: AgentIdentity;
84
- chatId: number;
336
+ chatId: SaltId;
85
337
  reason?: string;
86
338
  /** The shared chat's decrypted transcript, polled for up to ~20s until
87
339
  * the outgoing agent's HANDOFF_BRIEFING_MARKER shows up (or timeout). */
88
340
  context: string;
341
+ /** This identity's memory of `chatId`, seeded from the outgoing agent's
342
+ * session note (see sessions.ts's SESSION_NOTE_MARKER) when the briefing
343
+ * carried one. Whatever you reply() with here (your introduction) is
344
+ * appended as an assistant turn and persisted, so it's already in
345
+ * transcriptTail by the time the next onMessage call for this chat runs. */
346
+ session: Session;
347
+ reply(text: string): Promise<void>;
348
+ /** See MessageContext.ask. */
349
+ ask(question: string, opts?: AskOptions): Promise<AskResult>;
350
+ /** See MessageContext.approve. */
351
+ approve(summary: string, opts?: {
352
+ timeoutMs?: number;
353
+ }): Promise<{
354
+ approved: boolean;
355
+ by: SaltId;
356
+ via: "button" | "message";
357
+ }>;
358
+ /** See MessageContext.shareIdentity. */
359
+ shareIdentity(keys: string[], opts?: identityShare.ShareOptions): Promise<identityShare.ShareResult>;
360
+ }
361
+ export interface MandateOfferedContext {
362
+ identity: AgentIdentity;
363
+ mandate: Mandate;
364
+ /** Accepts THIS mandate (`client.mandates.accept`) -- binds this identity's current key
365
+ * fingerprint and moves it to `active`. Call it, or don't: an offer left untouched just stays
366
+ * `proposed` until someone accepts it or the 7-day proposal window lapses. */
367
+ accept(): Promise<Mandate>;
368
+ }
369
+ export interface MandateActivatedContext {
370
+ identity: AgentIdentity;
371
+ mandate: Mandate;
372
+ }
373
+ export interface MandatePausedContext {
374
+ identity: AgentIdentity;
375
+ mandate: Mandate;
376
+ }
377
+ export interface MandateRevokedContext {
378
+ identity: AgentIdentity;
379
+ mandate: Mandate;
380
+ }
381
+ export interface ApprovalRequestedContext {
382
+ identity: AgentIdentity;
383
+ exercise: MandateExercise;
384
+ /** Approve or deny the ask (`client.mandates.decide`). Fires only when THIS identity is the
385
+ * mandate's principal (e.g. a governance mandate's `money.pay` ask on an owned agent's own
386
+ * account) -- the delegate that made the original call gets `onApprovalDecided` instead, once
387
+ * this resolves it one way or the other. */
388
+ decide(decision: "approve" | "deny", note?: string): Promise<MandateExercise>;
389
+ }
390
+ export interface ApprovalDecidedContext {
391
+ identity: AgentIdentity;
392
+ /** Already decided (`exercise.decision` is `"approved"` or `"denied"`) -- informational, so the
393
+ * delegate that made the original ask-mode call can resume (or give up on) whatever it was
394
+ * doing. */
395
+ exercise: MandateExercise;
396
+ }
397
+ /** The `end_reason` of a device session. `lost_contact` is the one this SDK names: the computer stopped answering for 15 s mid-session. Any other value is passed through verbatim. */
398
+ export declare const DEVICE_LOST_CONTACT = "lost_contact";
399
+ /** Who a device-lane message must be encrypted for: `{ids}` (account ids, sorted, the caller excluded) or `{open: true}`. */
400
+ export interface DeviceChatBlock {
401
+ id: SaltId;
402
+ readers: {
403
+ ids?: SaltId[];
404
+ open?: boolean;
405
+ };
406
+ }
407
+ /** The body of a `device_session_ended` delivery: the session row plus `type`/`event`/`session_id`. Every field beyond `type`, `session_id` and `device_id` may be absent on an older server. */
408
+ export interface DeviceSessionEndedBody {
409
+ type: "device_session_ended";
410
+ event?: "ended";
411
+ session_id: SaltId;
412
+ id?: SaltId;
413
+ agent_id?: SaltId;
414
+ device_id: SaltId;
415
+ mandate_id?: SaltId | null;
416
+ /** The device LANE (where commands and results travelled), not the person's 1:1. */
417
+ chat_id?: SaltId | null;
418
+ status?: string;
419
+ intent?: string | null;
420
+ end_reason?: string | null;
421
+ requested_at?: string | null;
422
+ started_at?: string | null;
423
+ ended_at?: string | null;
424
+ chat?: DeviceChatBlock;
425
+ [key: string]: unknown;
426
+ }
427
+ /** The body of a `device_back` delivery: sent once per session that ended `lost_contact`, within 24 h, while the mandate is still active, the first time the computer is seen alive again. */
428
+ export interface DeviceBackBody {
429
+ type: "device_back";
430
+ device_id: SaltId;
431
+ device_name: string;
432
+ previous_session_id: SaltId;
433
+ /** When Salt ended that session for lost contact. */
434
+ offline_at: string | null;
435
+ /** When the computer was seen alive again. */
436
+ back_at: string;
437
+ /** The person's 1:1 with this agent; null when they have none. */
438
+ chat_id: SaltId | null;
439
+ chat?: DeviceChatBlock;
440
+ [key: string]: unknown;
441
+ }
442
+ /**
443
+ * A device session this identity ran just ended. Fires for EVERY end_reason;
444
+ * check `lostContact` (end_reason `"lost_contact"`) for the one that needs
445
+ * words. There is no `reply()` on purpose: `laneChatId` is the device lane
446
+ * the work travelled in, which the person does not read. Say it in the chat
447
+ * the work came from (remember it when you open the session) or their 1:1.
448
+ * Etiquette is in AGENTS.md ("A computer going quiet"): say it once per
449
+ * `sessionId`, plainly, and say that nothing else was sent. Never claim an
450
+ * action landed unless a device tool returned it.
451
+ */
452
+ export interface DeviceSessionEndedContext {
453
+ identity: AgentIdentity;
454
+ sessionId: SaltId;
455
+ deviceId: SaltId;
456
+ mandateId: SaltId | null;
457
+ /** The device lane, NOT a chat to answer the person in. */
458
+ laneChatId: SaltId | null;
459
+ endReason: string | null;
460
+ /** `endReason === "lost_contact"`: the computer stopped answering for 15 s mid-session. */
461
+ lostContact: boolean;
462
+ intent: string | null;
463
+ endedAt: string | null;
464
+ /** The delivery verbatim. */
465
+ body: DeviceSessionEndedBody;
466
+ }
467
+ /**
468
+ * The computer that went quiet during one of this identity's sessions is
469
+ * answering again. Grants nothing: open a NEW session to continue. Offer to
470
+ * finish, and look at the screen before assuming the last step landed. Fires
471
+ * once per lost session; say it once.
472
+ */
473
+ export interface DeviceBackContext {
474
+ identity: AgentIdentity;
475
+ deviceId: SaltId;
476
+ deviceName: string;
477
+ /** The session that ended `lost_contact` -- also the key to say this once. */
478
+ previousSessionId: SaltId;
479
+ offlineAt: string | null;
480
+ backAt: string;
481
+ /** The person's 1:1 with this agent, or null when they have none (then `reply()` throws). */
482
+ chatId: SaltId | null;
483
+ /** This identity's memory of `chatId`; null when `chatId` is. Whatever you reply() with is appended as an assistant turn and persisted. */
484
+ session: Session | null;
485
+ /** The delivery verbatim. */
486
+ body: DeviceBackBody;
487
+ /** Encrypts `text` for the 1:1's members and posts it. Throws when `chatId` is null. */
89
488
  reply(text: string): Promise<void>;
90
489
  }
490
+ /** A `describe_yourself` delivery: Salt asks this agent to write its own public description (the
491
+ * text shown on its profile, card and in the directory). Write it with `ctx.write(text)` -- that is
492
+ * `PATCH /api/v1/identity/sections {bio}` by this identity's own api-key; plain text, at most
493
+ * `maxLength` (300) characters. No chat is involved and no reply is read. */
494
+ export interface DescribeYourselfContext {
495
+ identity: AgentIdentity;
496
+ reason: "created" | "owner_changed" | "owner_asked" | "backfill" | string;
497
+ agent: {
498
+ id: SaltId;
499
+ username: string;
500
+ display_name: string;
501
+ category?: string | null;
502
+ };
503
+ /** What the owner wrote about this agent. Private to the owner and the agent; null when blank. */
504
+ ownerDescription: string | null;
505
+ /** What the public sees now (null when nothing is set). */
506
+ currentDescription: string | null;
507
+ capabilities: unknown[];
508
+ /** Salt's own instruction for how to write it. */
509
+ guidance: string;
510
+ maxLength: number;
511
+ /** Write the description (trimmed to `maxLength`); resolves once Salt has saved it. */
512
+ write(text: string): Promise<void>;
513
+ }
91
514
  export interface Logger {
92
515
  info(msg: string): void;
93
516
  error(msg: string): void;
@@ -96,29 +519,195 @@ export interface WebhookServerOptions {
96
519
  client: SaltClient;
97
520
  identities: IdentityStore;
98
521
  pgpPassphrase: string;
99
- /** Required in production: must match the X-Salt-Webhook-Secret header
100
- * salt-api's jobs send. Unset (local dev) skips the check entirely --
101
- * encrypted-message webhooks fail safe regardless (forged ciphertext
102
- * never decrypts), but card_interaction/invoice_paid payloads are
103
- * plaintext and would otherwise be actable by anyone who can reach this server. */
104
- webhookSharedSecret?: string;
522
+ /** Verify the HMAC signature salt-api sends on every callback.
523
+ *
524
+ * Defaults ON. Each identity has its OWN signing key, fetched from salt-api
525
+ * with that identity's api key and cached. This replaced a single fleet-wide
526
+ * WEBHOOK_SHARED_SECRET: because anyone could register an agent pointing at
527
+ * their own server, that secret leaked to any caller who asked for it, and a
528
+ * secret shared with the parties you authenticate against cannot prove who
529
+ * sent a request.
530
+ *
531
+ * Encrypted-message webhooks fail safe regardless (forged ciphertext never
532
+ * decrypts), but card_interaction/invoice_paid/chat_opened payloads are
533
+ * PLAINTEXT and would otherwise be actable by anyone who can reach this
534
+ * server.
535
+ *
536
+ * Set false only for local development against a dev salt-api. */
537
+ verifySignatures?: boolean;
538
+ /** Reject signatures older than this, so a captured POST can't be replayed. */
539
+ signatureToleranceSeconds?: number;
105
540
  /** This process's configured Mediator identity, if any -- enables the
106
541
  * "observe the shared chat silently, only speak in private coaching
107
542
  * chats" gate. */
108
- mediatorAgentId?: number;
543
+ mediatorAgentId?: SaltId;
544
+ /** How many messages from AGENT senders one chat may deliver before this
545
+ * process stops answering them there (the count resets whenever a person
546
+ * speaks). Default 2: the runaway guard for two hosted agents replying to
547
+ * each other. Raise it for a host whose agent legitimately talks with an
548
+ * allow-listed agent (a Claude Code channel bridge, say); a positive
549
+ * integer. Consult lanes keep their own, higher ceiling. */
550
+ maxAgentToAgentReplies?: number;
551
+ /** Where each hosted identity's per-chat session (recent turns + a short
552
+ * note) is kept. Defaults to an in-memory store (lost on restart --
553
+ * onMessage still gets a session on a cold start, rebuilt from the
554
+ * chat's own history). Pass `sessions.FileSessionStore(dir)` to persist
555
+ * across restarts; see sessions.ts for the trust boundary that store's
556
+ * own doc comment states plainly (plaintext on disk, no extra
557
+ * encryption -- the same trust level identities.json already assumes). */
558
+ sessionStore?: SessionStore;
109
559
  logger?: Logger;
110
560
  onMessage?: (ctx: MessageContext) => Promise<void> | void;
111
561
  onCardInteraction?: (ctx: CardInteractionContext) => Promise<void> | void;
562
+ /**
563
+ * Someone tapped `salt.ask` inside an app this identity is the
564
+ * installation's agent for (`POST /api/v1/app_installations/:id/ask`) --
565
+ * see AppActionContext. The usual answer is `client.apps.state`/
566
+ * `setState` (change the shared data) and/or `ctx.reply` (say something
567
+ * in the chat); `salt.ask`'s own promise on the app side resolves as soon
568
+ * as the webhook is accepted, regardless of what this handler does.
569
+ */
570
+ onAppAction?: (ctx: AppActionContext) => Promise<void> | void;
112
571
  onInvoicePaid?: (ctx: InvoicePaidContext) => Promise<void> | void;
572
+ onChatOpened?: (ctx: ChatOpenedContext) => Promise<void> | void;
113
573
  onHandoffConfirmed?: (ctx: HandoffConfirmedContext) => Promise<void> | void;
114
574
  onHandoffReceived?: (ctx: HandoffReceivedContext) => Promise<void> | void;
575
+ /** A mandate was offered to this identity and is waiting to be accepted. See
576
+ * MandateOfferedContext -- call `ctx.accept()` to accept it. */
577
+ onMandateOffered?: (ctx: MandateOfferedContext) => Promise<void> | void;
578
+ /** A mandate this identity is party to (either side) just became active. */
579
+ onMandateActivated?: (ctx: MandateActivatedContext) => Promise<void> | void;
580
+ /** A mandate this identity is party to was paused (e.g. the grantee's key changed). */
581
+ onMandatePaused?: (ctx: MandatePausedContext) => Promise<void> | void;
582
+ /** A mandate this identity is party to was revoked. */
583
+ onMandateRevoked?: (ctx: MandateRevokedContext) => Promise<void> | void;
584
+ /** This identity is the PRINCIPAL of an ask-mode call someone acting for it just made, and it's
585
+ * waiting on a decision. See ApprovalRequestedContext -- call `ctx.decide("approve"|"deny")`. */
586
+ onApprovalRequested?: (ctx: ApprovalRequestedContext) => Promise<void> | void;
587
+ /** This identity made the original ask-mode call and its exercise was just decided. */
588
+ onApprovalDecided?: (ctx: ApprovalDecidedContext) => Promise<void> | void;
589
+ /** Salt asked this agent to write its own public description (`describe_yourself`). The default is
590
+ * a no-op: an agent that does not handle it keeps whatever description its owner gave it. See
591
+ * DescribeYourselfContext -- call `ctx.write(text)`. */
592
+ onDescribeYourself?: (ctx: DescribeYourselfContext) => Promise<void> | void;
593
+ /**
594
+ * An incoming `[[SALT-IDENTITY-ASK]]` -- someone asking this identity to
595
+ * share one or more of its own card sections. Return the subset of
596
+ * `info.keys` to share (this replies with a signed SLICE) or `null`/
597
+ * `false` to decline (this replies with a DECLINE); either way the ask
598
+ * is intercepted and never reaches `onMessage`. Leave this unset and an
599
+ * incoming ask is NOT intercepted at all -- it reaches `onMessage` as
600
+ * ordinary text with just the marker line stripped, unanswered. See
601
+ * identityShare.ts.
602
+ */
603
+ onIdentityAsk?: IdentityAskHandler;
604
+ /**
605
+ * An incoming `[[SALT-IDENTITY-SLICE]]`, `[[SALT-IDENTITY-DECLINE]]` or
606
+ * `[[SALT-IDENTITY-REVOKE]]` from someone else -- always intercepted
607
+ * (never reaches `onMessage`) regardless of whether this is set. A SLICE
608
+ * is verified against the sender's own public key and, when verified,
609
+ * recorded on that chat's session (`session.identity[senderId]`, see
610
+ * sessions.ts) before this fires; a REVOKE removes any slice recorded
611
+ * under the same id first. See identityShare.ts's IdentitySharedEvent.
612
+ */
613
+ onIdentityShared?: IdentitySharedHandler;
614
+ /**
615
+ * An incoming `[[SALT-WORK-CANCEL id=...]]` (work.ts) -- always
616
+ * intercepted, never reaches `onMessage`, whether or not this is set. The
617
+ * usual answer is a fresh report under the same id with
618
+ * `status: "cancelled"` (see `createWorkReporter`'s `report`, or
619
+ * `client.postMessage` directly for a lower-level answer). Leave unset
620
+ * and a cancel is silently swallowed rather than answered -- the sender
621
+ * still never sees the marker itself.
622
+ */
623
+ onWorkCancel?: (ctx: WorkCancelContext) => Promise<void> | void;
624
+ /**
625
+ * A device session this identity ran just ended (`device_session_ended`;
626
+ * fires for every end_reason, see DeviceSessionEndedContext.lostContact).
627
+ * Not a chat message: it never reaches `onMessage`. Ignored when the
628
+ * delivery is the DEVICE's own copy (the computer hosting its own
629
+ * session), which this process only sees if it hosts the device identity.
630
+ */
631
+ onDeviceSessionEnded?: (ctx: DeviceSessionEndedContext) => Promise<void> | void;
632
+ /**
633
+ * The computer that stopped answering mid-session is back
634
+ * (`device_back`): sent once per lost session, within 24 h, while the
635
+ * mandate is active. See DeviceBackContext.
636
+ */
637
+ onDeviceBack?: (ctx: DeviceBackContext) => Promise<void> | void;
638
+ /**
639
+ * A device-control session lifecycle delivery (behind salt-api's
640
+ * `Feature :remote_control`): the body's `type` is
641
+ * `device_session_request` | `device_session_active` | `device_session_ended`,
642
+ * carrying the session metadata (ids, caps, budget, chat {id, readers}) the
643
+ * device needs to build its host. These are NOT chat messages and never reach
644
+ * `onMessage`; the device client (salt-device) passes them straight to its
645
+ * session controller. The actual command/result traffic rides the device lane
646
+ * as ordinary encrypted messages, which DO arrive through `onMessage`. The
647
+ * `type` is forwarded verbatim as the first argument. The device's own rail also
648
+ * carries `device_queue`, `device_grants_changed`, `device_owner_changed` and
649
+ * `device_ask_answered` (`{ask_id, decision, device_id, session_id}`: the person's
650
+ * phone answered a command or session ask; the answer is authenticated as the
651
+ * person by Salt). See src/device/.
652
+ */
653
+ onDeviceDelivery?: (type: string, body: Record<string, unknown>) => Promise<void> | void;
654
+ /**
655
+ * A message on a device-control lane (`chat.lane_kind === "device"`) -- the
656
+ * wire protocol between a controlling agent and a device. Set by the DEVICE
657
+ * (salt-device's sidecar): such a message is routed here right after the
658
+ * own-message check, BEFORE the mention gate, the agent-to-agent reply
659
+ * cap and session persistence (a command carries no mention, and a device
660
+ * lane would otherwise stop after two commands), and never reaches
661
+ * `onMessage`. Not awaited: a long action must not hold up the next
662
+ * delivery (the device host orders commands itself and must read `cancel`
663
+ * while one runs). The hook is the gate for WHO may command: check the
664
+ * sender against the session's agent. Unset, a device lane is handled like
665
+ * any other chat (the controlling AGENT reads results through `onMessage`).
666
+ */
667
+ onDeviceLaneMessage?: (ctx: DeviceLaneMessage) => Promise<void> | void;
115
668
  /** Extra fields to merge into the /health JSON response (e.g. which model is configured). */
116
669
  healthExtra?: () => Record<string, unknown>;
117
670
  }
671
+ /**
672
+ * Everything about the Salt protocol that is NOT "how do bytes get to this
673
+ * process" -- signature verification and the decrypt/route/dedup/reply
674
+ * logic for every webhook event kind. `createWebhookServer` below wraps
675
+ * this in an Express POST route (the only transport before K2); socket.ts's
676
+ * `createSocketClient` wraps the SAME dispatcher around a websocket push
677
+ * connection instead, so a consumer switches delivery mode with no change
678
+ * to onMessage/onCardInteraction/etc. -- see socket.ts's own doc comment.
679
+ */
680
+ export declare function createDispatcher(options: WebhookServerOptions): Dispatcher;
681
+ /** What createDispatcher returns -- the two things any transport (an
682
+ * Express POST route, socket.ts's websocket push client) needs: verify an
683
+ * envelope's signature, then hand its body to the right handler. */
684
+ export interface Dispatcher {
685
+ verifyEnvelope(agentId: string | undefined, signature: string | undefined, rawBody: string, opts?: {
686
+ toleranceSeconds?: number;
687
+ }): Promise<{
688
+ ok: true;
689
+ } | {
690
+ ok: false;
691
+ reason: string;
692
+ transient: boolean;
693
+ }>;
694
+ dispatch(body: Record<string, unknown>, headerAgentId?: SaltId): Promise<void>;
695
+ /** Settles a pending ctx.ask / delegation wait with this (already
696
+ * verified) body if it is the answer, returning true when consumed. Never
697
+ * runs a handler, so a transport whose handlers block its frame queue
698
+ * (socket.ts) can call it ahead of that queue. */
699
+ resolveAnswer(body: Record<string, unknown>, headerAgentId?: SaltId): Promise<boolean>;
700
+ }
118
701
  /**
119
702
  * Creates the Express app for a Salt agent's webhook endpoint. Mount it
120
703
  * yourself (e.g. `createWebhookServer(opts).listen(port)`, or grab `.app`
121
704
  * to compose it into a bigger server).
705
+ *
706
+ * A thin transport wrapper around createDispatcher -- everything about the
707
+ * Salt protocol itself lives there. Switching a consumer from this to
708
+ * socket.ts's createSocketClient (K2, an agent with no public URL) is the
709
+ * same `options` object with `.listen(port)` replaced by `.start()`;
710
+ * onMessage/onCardInteraction/etc. never change.
122
711
  */
123
712
  export declare function createWebhookServer(options: WebhookServerOptions): {
124
713
  app: Express;