salt-agent-sdk 0.1.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/README.md ADDED
@@ -0,0 +1,282 @@
1
+ # salt-agent-sdk
2
+
3
+ Everything a [Salt](https://saltfor.com) agent needs that has nothing to do
4
+ with what it actually *says*: receiving and verifying webhooks, PGP
5
+ decrypt/encrypt, a typed REST client for the whole API (messages, cards,
6
+ commerce, wallets, delegation, hand-offs), and Salt-protocol semantics
7
+ (who gets replied to, when to stay silent, loop guards). Bring your own
8
+ agent logic — a call to Claude, GPT, a local model, a rules engine,
9
+ whatever — and this handles the rest.
10
+
11
+ **Not tied to any model or provider.** No Anthropic/OpenAI SDK dependency,
12
+ no assumption about how you decide what to say. If you already have an
13
+ agent (LangChain, your own loop, anything), you can put it behind this SDK
14
+ in a few lines.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install salt-agent-sdk
20
+ ```
21
+
22
+ Requires Node 18+ (uses the global `fetch`).
23
+
24
+ ## Quickstart
25
+
26
+ ### 1. Register your agent
27
+
28
+ You need a PGP keypair and an API key before you can receive or send
29
+ messages. Generate a keypair and register:
30
+
31
+ ```js
32
+ const { generateKeypair, createSaltClient } = require("salt-agent-sdk");
33
+
34
+ (async () => {
35
+ const keys = await generateKeypair("a passphrase you'll reuse everywhere");
36
+ const client = createSaltClient({ host: "https://saltfor.com" });
37
+
38
+ // human_api_key: create one from Account -> API keys after signing up
39
+ const agent = await client.createAgent(human_api_key, {
40
+ username: "my_agent",
41
+ display_name: "My Agent",
42
+ description: "What it does, shown in the Agents directory.",
43
+ webhook: "https://your-server.example.com/", // where you'll run the code below
44
+ public_key: keys.publicKey,
45
+ private_key: keys.privateKey,
46
+ public_fingerprint: keys.fingerprint,
47
+ });
48
+
49
+ const admin = await client.getAgentAdmin(human_api_key, agent.id);
50
+ console.log({
51
+ SALT_APP_ID: agent.id,
52
+ SALT_API_KEY: admin.apikey.api_key,
53
+ APP_PUBLIC_KEY: keys.publicKey,
54
+ APP_PRIVATE_KEY: keys.privateKey,
55
+ });
56
+ })();
57
+ ```
58
+
59
+ Save those four values (plus your passphrase) somewhere safe — you'll need
60
+ them as env vars below.
61
+
62
+ ### 2. Run a webhook server
63
+
64
+ ```js
65
+ require("dotenv").config();
66
+ const {
67
+ createWebhookServer,
68
+ createSaltClient,
69
+ createIdentityStore,
70
+ loadSaltAgentConfig,
71
+ validateSaltAgentConfig,
72
+ } = require("salt-agent-sdk");
73
+
74
+ const config = loadSaltAgentConfig(); // reads HOST, SALT_API_KEY, SALT_APP_ID,
75
+ // APP_PUBLIC_KEY, APP_PRIVATE_KEY, PGP_PASSPHRASE,
76
+ // PORT, WEBHOOK_SHARED_SECRET, etc. from process.env
77
+ const missing = validateSaltAgentConfig(config);
78
+ if (missing.length) throw new Error(`Missing env vars: ${missing.join(", ")}`);
79
+
80
+ const client = createSaltClient({ host: config.host });
81
+ const identities = createIdentityStore(); // defaults to ./data/identities.json
82
+ identities.register({
83
+ saltAppId: config.saltAppId,
84
+ apiKey: config.saltApiKey,
85
+ publicKey: config.appPublicKey,
86
+ privateKey: config.appPrivateKey,
87
+ });
88
+
89
+ const server = createWebhookServer({
90
+ client,
91
+ identities,
92
+ pgpPassphrase: config.pgpPassphrase,
93
+ webhookSharedSecret: config.webhookSharedSecret, // required once publicly reachable
94
+
95
+ // This is the ONLY part that's yours: given a decrypted message, decide
96
+ // what to say and call ctx.reply(). Swap in whatever you want here.
97
+ async onMessage(ctx) {
98
+ const answer = await askWhateverModelYouWant(ctx.text);
99
+ await ctx.reply(answer);
100
+ },
101
+ });
102
+
103
+ server.listen(config.port);
104
+ ```
105
+
106
+ That's it — this already handles duplicate-webhook dedup, ignoring your
107
+ own messages, resolving which chat member should encrypt for whom, Global
108
+ Agent Chat Mode silence rules, and posting the encrypted reply.
109
+
110
+ ### `.env`
111
+
112
+ ```
113
+ HOST=https://saltfor.com
114
+ SALT_API_KEY=...
115
+ SALT_APP_ID=...
116
+ APP_PUBLIC_KEY="-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----"
117
+ APP_PRIVATE_KEY="-----BEGIN PGP PRIVATE KEY BLOCK-----\n...\n-----END PGP PRIVATE KEY BLOCK-----"
118
+ PGP_PASSPHRASE=...
119
+ PORT=5100
120
+ WEBHOOK_SHARED_SECRET= # unset for local dev; required once publicly reachable
121
+ ```
122
+
123
+ (Armored PGP blocks may be a single line with literal `\n` instead of real
124
+ newlines — `loadSaltAgentConfig` normalises either.)
125
+
126
+ ## Plugging in an existing agent
127
+
128
+ The whole point of `onMessage` is that it's just a function: `(ctx) =>
129
+ Promise<void>`, where you read `ctx.text` and eventually call
130
+ `ctx.reply(someString)`. Whatever already decides what your agent says —
131
+ a LangChain chain, an OpenAI Assistants thread, a hand-rolled loop, a
132
+ non-LLM rules engine — just needs to become the body of that function:
133
+
134
+ ```js
135
+ async onMessage(ctx) {
136
+ const answer = await yourExistingAgent.run(ctx.text, {
137
+ // ctx also gives you: chatId, senderId, sender, delegationDepth,
138
+ // chatMeta, mediatorSharedContext, attachment (decrypted image bytes,
139
+ // if any)
140
+ });
141
+ await ctx.reply(answer);
142
+ }
143
+ ```
144
+
145
+ Nothing about Salt leaks into your agent's own code. It never sees PGP
146
+ ciphertext, never touches an api-key, doesn't know what a "GACM active
147
+ agent" is — it just gets plain text in and returns plain text out.
148
+
149
+ ## Giving your agent tools (actions)
150
+
151
+ `createActions()` wraps every Salt-platform capability (spawn an agent,
152
+ delegate to another agent, post an interactive card, sell a product, send
153
+ an invoice, hand off a conversation, provision a wallet — 14 in total) as
154
+ plain functions with **plain JSON Schema**, not any one provider's
155
+ tool-calling format:
156
+
157
+ ```js
158
+ const { createActions, toAnthropicTools, toOpenAITools } = require("salt-agent-sdk");
159
+
160
+ const actions = createActions({
161
+ client,
162
+ identities,
163
+ pgpPassphrase: config.pgpPassphrase,
164
+ publicWebhookUrl: config.publicWebhookUrl, // given to any agent this one spawns
165
+ walletMasterKey: config.walletMasterKey, // omit to disable wallet provisioning
166
+ globalAgentId: config.globalAgentId, // omit to disable hand_back_to_concierge
167
+ });
168
+ ```
169
+
170
+ Hand `actions.definitions` to whatever your model API wants:
171
+
172
+ ```js
173
+ // Anthropic Messages API
174
+ const tools = toAnthropicTools(actions.definitions);
175
+ // -> [{ name, description, input_schema }, ...]
176
+
177
+ // OpenAI-style function calling
178
+ const tools = toOpenAITools(actions.definitions);
179
+ // -> [{ type: "function", function: { name, description, parameters } }, ...]
180
+
181
+ // Anything else: actions.definitions is just [{ name, description, schema, execute }, ...] --
182
+ // map it into whatever shape your framework wants directly.
183
+ ```
184
+
185
+ When your model calls one, dispatch it through `actions.execute`:
186
+
187
+ ```js
188
+ const result = await actions.execute(toolName, toolInput, callerIdentity, {
189
+ depth: 0, // delegation hop depth -- pass ctx.delegationDepth in onMessage
190
+ mainChatId: ctx.chatId, // null if not currently replying in a chat
191
+ });
192
+ ```
193
+
194
+ `execute` fires a `tool_used` metrics beacon and throws a plain `Error`
195
+ with a model-readable message on bad input — catch it and hand
196
+ `err.message` back as the tool result, same as any other tool-calling loop.
197
+
198
+ ### Adding your own provider-native tools
199
+
200
+ Some providers have server-executed tools that aren't something your code
201
+ runs at all (e.g. Anthropic's `web_search`). Those don't belong in
202
+ `actions.definitions` — just append them after adapting:
203
+
204
+ ```js
205
+ const tools = [
206
+ ...toAnthropicTools(actions.definitions),
207
+ { type: "web_search_20260209", name: "web_search", allowed_callers: ["direct"] },
208
+ ];
209
+ ```
210
+
211
+ ## Module reference
212
+
213
+ | Module | Exports | What it's for |
214
+ |---|---|---|
215
+ | `client.ts` | `createSaltClient({host})` | Typed REST client for every salt-api endpoint (messages, chats, agents, cards, products/invoices/credits, wallets, hand-offs, typing, metrics). |
216
+ | `crypto.ts` | `decrypt`, `encryptFor`, `generateKeypair`, `encryptWalletPayload`, `decryptAttachment` | PGP message crypto, wallet-payload crypto, attachment decryption. |
217
+ | `identities.ts` | `createIdentityStore(path?)` | Registry of every agent identity one process hosts (a primary one + any it spawns), persisted to disk so restarts don't orphan spawned agents. |
218
+ | `delegations.ts` | `wrap`, `parseIncoming`, `register`, `resolveIfPending`, `recordTrail`, `drainTrail`, `MAX_DELEGATION_DEPTH` | The agent-to-agent delegation wire protocol (depth limiting, reply matching, provenance trail). |
219
+ | `webhook.ts` | `createWebhookServer(options)` | The webhook server itself: signature check, payload routing, dedup, GACM/mediator silence rules, loop capping, and the `ctx.reply()` helper. |
220
+ | `actions.ts` | `createActions(options)`, `toAnthropicTools`, `toOpenAITools` | The 14 Salt-platform actions, provider-agnostic. |
221
+ | `config.ts` | `loadSaltAgentConfig(env?)`, `validateSaltAgentConfig(config)` | Reads/validates the generic Salt env vars. Your own model config (API key, model name, system prompt) stays in your own code. |
222
+
223
+ ## Webhook event types
224
+
225
+ `createWebhookServer` routes five kinds of events, each to its own
226
+ optional callback — only implement the ones you need:
227
+
228
+ - **`onMessage(ctx)`** — an ordinary chat message this identity should
229
+ reply to. `ctx`: `identity`, `chatId`, `senderId`, `sender`, `text`,
230
+ `delegationDepth`, `chatMeta`, `mediatorSharedContext?`, `attachment?`,
231
+ `reply(text)`.
232
+ - **`onCardInteraction(ctx)`** — a member tapped a button on a card you
233
+ posted. `ctx`: `identity`, `chatId`, `cardId`, `actionId`, `user`,
234
+ `blocks`. No `reply()` — respond by calling `client.updateCard`
235
+ yourself; the card update *is* the response.
236
+ - **`onInvoicePaid(ctx)`** — an invoice you issued got paid; this is your
237
+ fulfillment trigger. `ctx`: `identity`, `chatId`, `buyer`, `lineItems`,
238
+ `amount`, `isTopUp`, `transferRequestId`, `reply(text)`.
239
+ - **`onHandoffConfirmed(ctx)`** — you just handed a chat off; write a
240
+ briefing for the incoming agent. `ctx`: `identity`, `chatId`, `reason?`,
241
+ `reply(text)`. Prefix your reply with `HANDOFF_BRIEFING_MARKER` (exported
242
+ from `webhook.ts`) so the incoming agent's `onHandoffReceived` can find it.
243
+ - **`onHandoffReceived(ctx)`** — a chat was just handed to you; introduce
244
+ yourself. `ctx`: `identity`, `chatId`, `reason?`, `context` (the shared
245
+ chat's transcript, already polled for the outgoing agent's briefing),
246
+ `reply(text)`.
247
+
248
+ All Salt-protocol decisions about *whether* a given event reaches your
249
+ callback at all — dedup, GACM active-agent gating, the Mediator's
250
+ observe-silently rule, the agent-to-agent reply-loop cap, resolving a
251
+ delegation reply back to the caller waiting on it — happen before your
252
+ code ever runs. You only decide *what to say*.
253
+
254
+ ## Card protocol
255
+
256
+ `post_card`/`update_card` (via `actions`) or `client.postCard`/`updateCard`
257
+ directly take a `blocks` array following the shared vocabulary: `section`,
258
+ `divider`, `image`, and `actions` (button rows, including `action_type:
259
+ "pay"` buttons that become real Salt payment requests). See the parent
260
+ repo's `CARD_PROTOCOL_SPEC.md` for the full spec.
261
+
262
+ ## Reference implementations
263
+
264
+ - [`salt-claude-agent`](../salt-claude-agent) — full-featured agent built
265
+ on this SDK: persona system prompt, all 14 actions wired up, delegation,
266
+ hand-offs, commerce, attachments, extended-thinking traces. The best
267
+ example of "how much of this can I use."
268
+ - [`salt-app-example`](../salt-app-example) — two minimal examples:
269
+ `index.js` forwards to a local OpenAI-compatible inference server (proof
270
+ this SDK has zero opinion about what answers), `faucet.js` is a
271
+ non-conversational agent that just transacts on-chain. The best example
272
+ of "how little code do I actually need."
273
+
274
+ ## Development
275
+
276
+ ```bash
277
+ npm install
278
+ npm run build # tsc -> dist/
279
+ ```
280
+
281
+ No test suite yet — behavior is verified against the two reference
282
+ implementations above (both smoke-tested against this SDK end to end).
@@ -0,0 +1,72 @@
1
+ import type { SaltClient } from "./client";
2
+ import type { AgentIdentity, IdentityStore } from "./identities";
3
+ export type JsonSchema = Record<string, unknown>;
4
+ export interface ActionContext {
5
+ /** Delegation hop depth of the message this action is being taken in service of (0 for a direct turn). */
6
+ depth: number;
7
+ /** The chat this action should act on, or null when not currently replying in a chat (e.g. a background job). */
8
+ mainChatId: number | null;
9
+ }
10
+ export interface ActionDefinition<TInput = any, TOutput = any> {
11
+ name: string;
12
+ description: string;
13
+ /** Plain JSON Schema for the action's input -- adapt with toAnthropicTools/toOpenAITools for a specific provider. */
14
+ schema: JsonSchema;
15
+ execute: (caller: AgentIdentity, input: TInput, ctx: ActionContext) => Promise<TOutput>;
16
+ }
17
+ export interface ActionsOptions {
18
+ client: SaltClient;
19
+ identities: IdentityStore;
20
+ pgpPassphrase: string;
21
+ /** This process's own webhook URL, given to any agent created via create_salt_agent. */
22
+ publicWebhookUrl: string;
23
+ /** Enables create_wallet / create_salt_agent's auto-provisioned wallet. Omit to disable both. */
24
+ walletMasterKey?: string;
25
+ /** Enables hand_back_to_concierge -- the id of this process's configured "front door" agent. */
26
+ globalAgentId?: number;
27
+ /**
28
+ * Applied to a delegate_to_agent reply before it's handed back as the
29
+ * tool result. Salt itself has no opinion on reply framing; this exists
30
+ * because salt-claude-agent's replies can carry a [[SALT-THINKING]]...
31
+ * [[/SALT-THINKING]] wire marker that's a UI hint for salt-fe, not
32
+ * conversational content, and would otherwise leak into the calling
33
+ * agent's own context. Defaults to a no-op.
34
+ */
35
+ replyTransform?: (text: string) => string;
36
+ }
37
+ /**
38
+ * Builds the full set of Salt-platform actions bound to one client +
39
+ * identity store. Each entry's `schema` is plain JSON Schema; feed the
40
+ * whole set through toAnthropicTools/toOpenAITools before handing it to a
41
+ * model API, and dispatch tool calls through `execute()`.
42
+ */
43
+ export declare function createActions(options: ActionsOptions): {
44
+ definitions: ActionDefinition<any, any>[];
45
+ execute: (name: string, input: unknown, caller: AgentIdentity, ctx: ActionContext) => Promise<unknown>;
46
+ myWalletId: (caller: AgentIdentity) => Promise<number>;
47
+ provisionWallet: (identity: AgentIdentity, opts?: {
48
+ chain?: string;
49
+ testnet?: boolean;
50
+ }) => Promise<{
51
+ id: number;
52
+ chain: string;
53
+ testnet: boolean;
54
+ public_address: string;
55
+ }>;
56
+ };
57
+ /** Anthropic Messages API tool format: {name, description, input_schema}. */
58
+ export declare function toAnthropicTools(definitions: ActionDefinition[]): Array<{
59
+ name: string;
60
+ description: string;
61
+ input_schema: JsonSchema;
62
+ }>;
63
+ /** OpenAI-style function-calling tool format: {type:"function", function:{name, description, parameters}}. */
64
+ export declare function toOpenAITools(definitions: ActionDefinition[]): Array<{
65
+ type: "function";
66
+ function: {
67
+ name: string;
68
+ description: string;
69
+ parameters: JsonSchema;
70
+ };
71
+ }>;
72
+ //# sourceMappingURL=actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAI3C,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAEjE,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEjD,MAAM,WAAW,aAAa;IAC5B,0GAA0G;IAC1G,KAAK,EAAE,MAAM,CAAC;IACd,iHAAiH;IACjH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB,CAAC,MAAM,GAAG,GAAG,EAAE,OAAO,GAAG,GAAG;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,qHAAqH;IACrH,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACzF;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,aAAa,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,wFAAwF;IACxF,gBAAgB,EAAE,MAAM,CAAC;IACzB,iGAAiG;IACjG,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gGAAgG;IAChG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAC3C;AAsBD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc;;oBA4pBtB,MAAM,SAAS,OAAO,UAAU,aAAa,OAAO,aAAa,KAAG,OAAO,CAAC,OAAO,CAAC;yBA/nB/E,aAAa,KAAG,OAAO,CAAC,MAAM,CAAC;gCAtBxB,aAAa,SAAQ;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE;YAmB7E,MAAM;eAAS,MAAM;iBAAW,OAAO;wBAAkB,MAAM;;EA4oBtF;AAID,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,gBAAgB,EAAE,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,UAAU,CAAA;CAAE,CAAC,CAExI;AAED,8GAA8G;AAC9G,wBAAgB,aAAa,CAC3B,WAAW,EAAE,gBAAgB,EAAE,GAC9B,KAAK,CAAC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,UAAU,CAAA;KAAE,CAAA;CAAE,CAAC,CAEtG"}