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