@letta-ai/letta-code 0.34.12 → 0.34.13
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/dist/app-server-client.cjs +2 -2
- package/dist/app-server-client.cjs.map +3 -3
- package/dist/app-server-client.js +2 -2
- package/dist/app-server-client.js.map +3 -3
- package/dist/mcp-client.js +2 -2
- package/dist/mcp-client.js.map +1 -1
- package/dist/mcp-oauth.js +2 -2
- package/dist/mcp-oauth.js.map +1 -1
- package/dist/types/agent/skills.d.ts.map +1 -1
- package/dist/types/agent/subagents/index.d.ts.map +1 -1
- package/dist/types/auth/oauth.d.ts +2 -0
- package/dist/types/auth/oauth.d.ts.map +1 -1
- package/dist/types/tools/impl/external-coding-agent-task.d.ts +2 -1
- package/dist/types/tools/impl/external-coding-agent-task.d.ts.map +1 -1
- package/dist/types/tools/impl/external-coding-agent.d.ts +1 -1
- package/dist/types/tools/impl/external-coding-agent.d.ts.map +1 -1
- package/dist/types/tools/impl/kill-bash.d.ts.map +1 -1
- package/dist/types/tools/impl/monitor-backup.d.ts +43 -0
- package/dist/types/tools/impl/monitor-backup.d.ts.map +1 -0
- package/dist/types/tools/impl/monitor.d.ts +2 -0
- package/dist/types/tools/impl/monitor.d.ts.map +1 -1
- package/dist/types/tools/impl/opencode-server.d.ts +36 -0
- package/dist/types/tools/impl/opencode-server.d.ts.map +1 -0
- package/dist/types/tools/impl/opencode-session.d.ts +47 -0
- package/dist/types/tools/impl/opencode-session.d.ts.map +1 -0
- package/dist/types/tools/impl/remote-external-coding-agent.d.ts.map +1 -1
- package/dist/types/tools/impl/send-agent-message.d.ts.map +1 -1
- package/dist/types/types/app-server-info.d.ts +2 -0
- package/dist/types/types/app-server-info.d.ts.map +1 -1
- package/dist/types/types/file-write-protocol.d.ts +41 -0
- package/dist/types/types/file-write-protocol.d.ts.map +1 -0
- package/dist/types/types/protocol_v2.d.ts +2 -16
- package/dist/types/types/protocol_v2.d.ts.map +1 -1
- package/letta.js +4300 -3524
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scripts/source-file-size-baseline.json +2 -2
- package/skills/creating-skills/scripts/package-skill.ts +18 -8
- package/skills/managing-agent-webhooks/SKILL.md +305 -0
- package/skills/managing-agent-webhooks/scripts/manage-agent-webhooks.mjs +1364 -0
|
@@ -0,0 +1,1364 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { realpathSync } from "node:fs";
|
|
5
|
+
import { open, readFile, unlink } from "node:fs/promises";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const COMMANDS = new Set([
|
|
10
|
+
"create",
|
|
11
|
+
"delete",
|
|
12
|
+
"disable",
|
|
13
|
+
"enable",
|
|
14
|
+
"list",
|
|
15
|
+
"requests",
|
|
16
|
+
"rotate",
|
|
17
|
+
"test",
|
|
18
|
+
]);
|
|
19
|
+
const BOOLEAN_FLAGS = new Set([
|
|
20
|
+
"confirm",
|
|
21
|
+
"disabled",
|
|
22
|
+
"include-body",
|
|
23
|
+
"public",
|
|
24
|
+
"security-key-stdin",
|
|
25
|
+
"secure",
|
|
26
|
+
]);
|
|
27
|
+
const COMMAND_OPTIONS = new Map([
|
|
28
|
+
[
|
|
29
|
+
"create",
|
|
30
|
+
new Set([
|
|
31
|
+
"credential-output",
|
|
32
|
+
"disabled",
|
|
33
|
+
"name",
|
|
34
|
+
"preprompt",
|
|
35
|
+
"public",
|
|
36
|
+
"security-key-stdin",
|
|
37
|
+
"secure",
|
|
38
|
+
]),
|
|
39
|
+
],
|
|
40
|
+
["delete", new Set(["confirm", "webhook-id"])],
|
|
41
|
+
["disable", new Set(["confirm", "webhook-id"])],
|
|
42
|
+
["enable", new Set(["webhook-id"])],
|
|
43
|
+
["list", new Set()],
|
|
44
|
+
["requests", new Set(["include-body", "limit", "webhook-id"])],
|
|
45
|
+
["rotate", new Set(["confirm", "webhook-id"])],
|
|
46
|
+
[
|
|
47
|
+
"test",
|
|
48
|
+
new Set([
|
|
49
|
+
"confirm",
|
|
50
|
+
"credential-file",
|
|
51
|
+
"payload-file",
|
|
52
|
+
"security-key-stdin",
|
|
53
|
+
"webhook-id",
|
|
54
|
+
]),
|
|
55
|
+
],
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
function usage() {
|
|
59
|
+
console.error(`Usage:
|
|
60
|
+
node manage-agent-webhooks.mjs list
|
|
61
|
+
node manage-agent-webhooks.mjs create --name <name> (--public | --secure --credential-output <path> | --security-key-stdin) [--preprompt <text>] [--disabled]
|
|
62
|
+
node manage-agent-webhooks.mjs test --webhook-id <id> --confirm [--payload-file <path>] [--credential-file <path> | --security-key-stdin]
|
|
63
|
+
node manage-agent-webhooks.mjs requests --webhook-id <id> [--limit <1-50>] [--include-body]
|
|
64
|
+
node manage-agent-webhooks.mjs enable --webhook-id <id>
|
|
65
|
+
node manage-agent-webhooks.mjs disable --webhook-id <id> --confirm
|
|
66
|
+
node manage-agent-webhooks.mjs rotate --webhook-id <id> --confirm
|
|
67
|
+
node manage-agent-webhooks.mjs delete --webhook-id <id> --confirm
|
|
68
|
+
|
|
69
|
+
Environment:
|
|
70
|
+
LETTA_API_KEY Required for management operations
|
|
71
|
+
LETTA_BASE_URL Active server supplied by the runtime
|
|
72
|
+
AGENT_ID Required current agent
|
|
73
|
+
`);
|
|
74
|
+
process.exit(2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function parseAgentWebhookArgs(argv) {
|
|
78
|
+
const [command = "", ...rest] = argv;
|
|
79
|
+
if (!COMMANDS.has(command)) usage();
|
|
80
|
+
const allowedOptions = COMMAND_OPTIONS.get(command);
|
|
81
|
+
if (!allowedOptions) throw new Error(`Unsupported command: ${command}`);
|
|
82
|
+
const args = {};
|
|
83
|
+
for (let index = 0; index < rest.length; index++) {
|
|
84
|
+
const argument = rest[index];
|
|
85
|
+
if (argument === undefined) {
|
|
86
|
+
throw new Error("Unexpected end of arguments");
|
|
87
|
+
}
|
|
88
|
+
if (argument === "--help" || argument === "-h") usage();
|
|
89
|
+
if (!argument.startsWith("--")) {
|
|
90
|
+
throw new Error(`Unexpected positional argument: ${argument}`);
|
|
91
|
+
}
|
|
92
|
+
const key = argument.slice(2);
|
|
93
|
+
if (!allowedOptions.has(key)) {
|
|
94
|
+
throw new Error(`Unsupported option --${key} for ${command}`);
|
|
95
|
+
}
|
|
96
|
+
if (Object.hasOwn(args, key)) {
|
|
97
|
+
throw new Error(`Duplicate option --${key}`);
|
|
98
|
+
}
|
|
99
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
100
|
+
args[key] = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const value = rest[++index];
|
|
104
|
+
if (!value || value.startsWith("--")) {
|
|
105
|
+
throw new Error(`Missing value for --${key}`);
|
|
106
|
+
}
|
|
107
|
+
args[key] = value;
|
|
108
|
+
}
|
|
109
|
+
return { args, command };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function requireString(value, message) {
|
|
113
|
+
const stringValue = String(value ?? "").trim();
|
|
114
|
+
if (!stringValue) throw new Error(message);
|
|
115
|
+
return stringValue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function requireResponseString(value, message) {
|
|
119
|
+
if (typeof value !== "string" || !value.trim() || value !== value.trim()) {
|
|
120
|
+
throw new Error(message);
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function resolveAgentWebhookTarget(currentAgentId) {
|
|
126
|
+
return requireString(
|
|
127
|
+
currentAgentId,
|
|
128
|
+
"AGENT_ID is required; this helper only manages the current agent",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function safeAgentWebhook(webhook) {
|
|
133
|
+
const safe = /** @type {Record<string, unknown>} */ ({});
|
|
134
|
+
for (const field of [
|
|
135
|
+
"agent_id",
|
|
136
|
+
"created_at",
|
|
137
|
+
"id",
|
|
138
|
+
"name",
|
|
139
|
+
"preprompt",
|
|
140
|
+
"webhook_url",
|
|
141
|
+
]) {
|
|
142
|
+
if (typeof webhook[field] === "string") safe[field] = webhook[field];
|
|
143
|
+
}
|
|
144
|
+
for (const field of ["enabled", "requires_authorization_header"]) {
|
|
145
|
+
if (typeof webhook[field] === "boolean") safe[field] = webhook[field];
|
|
146
|
+
}
|
|
147
|
+
if (typeof webhook.disabled_at === "string" || webhook.disabled_at === null) {
|
|
148
|
+
safe.disabled_at = webhook.disabled_at;
|
|
149
|
+
}
|
|
150
|
+
return safe;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function buildAgentWebhookBasicAuthorization(securityKey) {
|
|
154
|
+
if (!securityKey) throw new Error("Security key must be non-empty");
|
|
155
|
+
return `Basic ${Buffer.from(`webhook:${securityKey}`, "utf8").toString("base64")}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function normalizedBaseUrl(value) {
|
|
159
|
+
const baseUrl = requireString(
|
|
160
|
+
value,
|
|
161
|
+
"LETTA_BASE_URL is required from the active runtime",
|
|
162
|
+
);
|
|
163
|
+
const parsed = new URL(baseUrl);
|
|
164
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
165
|
+
throw new Error("Base URL must use http or https");
|
|
166
|
+
}
|
|
167
|
+
const loopbackHostnames = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
168
|
+
if (parsed.protocol === "http:" && !loopbackHostnames.has(parsed.hostname)) {
|
|
169
|
+
throw new Error("Plaintext LETTA_BASE_URL is allowed only on loopback");
|
|
170
|
+
}
|
|
171
|
+
return parsed.toString().replace(/\/$/, "");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function validatedWebhookIngressUrl(webhook, baseUrl) {
|
|
175
|
+
const webhookUrl = requireResponseString(
|
|
176
|
+
webhook.webhook_url,
|
|
177
|
+
"Webhook response is missing webhook_url",
|
|
178
|
+
);
|
|
179
|
+
const webhookSlug = requireResponseString(
|
|
180
|
+
webhook.webhook_slug,
|
|
181
|
+
"Webhook response is missing webhook_slug",
|
|
182
|
+
);
|
|
183
|
+
if (!/^[A-Za-z0-9_-]+$/.test(webhookSlug)) {
|
|
184
|
+
throw new Error("Webhook response contains an invalid webhook_slug");
|
|
185
|
+
}
|
|
186
|
+
let parsed;
|
|
187
|
+
try {
|
|
188
|
+
parsed = new URL(webhookUrl);
|
|
189
|
+
} catch {
|
|
190
|
+
throw new Error("Webhook response contains an invalid webhook_url");
|
|
191
|
+
}
|
|
192
|
+
const managementOrigin = new URL(baseUrl).origin;
|
|
193
|
+
if (
|
|
194
|
+
parsed.origin !== managementOrigin ||
|
|
195
|
+
parsed.username ||
|
|
196
|
+
parsed.password ||
|
|
197
|
+
parsed.search ||
|
|
198
|
+
parsed.hash
|
|
199
|
+
) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
"Webhook URL must use the active runtime origin without credentials, query, or fragment",
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const expectedPath = `/v1/agent-webhooks/${webhookSlug}`;
|
|
205
|
+
if (parsed.pathname !== expectedPath) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
"Webhook URL path does not match the returned webhook_slug",
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
return parsed.toString();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const MAX_RESPONSE_BODY_BYTES = 1024 * 1024;
|
|
214
|
+
|
|
215
|
+
async function parseResponse(response, options = {}) {
|
|
216
|
+
const chunks = [];
|
|
217
|
+
let totalBytes = 0;
|
|
218
|
+
if (response.body) {
|
|
219
|
+
const reader = response.body.getReader();
|
|
220
|
+
while (true) {
|
|
221
|
+
const { done, value } = await reader.read();
|
|
222
|
+
if (done) break;
|
|
223
|
+
totalBytes += value.byteLength;
|
|
224
|
+
if (totalBytes > MAX_RESPONSE_BODY_BYTES) {
|
|
225
|
+
await reader.cancel();
|
|
226
|
+
if (options.allowOmitted === true) {
|
|
227
|
+
return { body: null, omitted: true };
|
|
228
|
+
}
|
|
229
|
+
throw new Error("Response body exceeds the 1 MiB read limit");
|
|
230
|
+
}
|
|
231
|
+
chunks.push(Buffer.from(value));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
235
|
+
let body = null;
|
|
236
|
+
if (text) {
|
|
237
|
+
try {
|
|
238
|
+
body = JSON.parse(text);
|
|
239
|
+
} catch {
|
|
240
|
+
body = text;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (options.allowOmitted === true) {
|
|
244
|
+
return { body, omitted: false };
|
|
245
|
+
}
|
|
246
|
+
return body;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const SAFE_ERROR_FIELDS = new Set([
|
|
250
|
+
"code",
|
|
251
|
+
"error",
|
|
252
|
+
"errorCode",
|
|
253
|
+
"error_code",
|
|
254
|
+
"message",
|
|
255
|
+
"requestId",
|
|
256
|
+
"request_id",
|
|
257
|
+
]);
|
|
258
|
+
function fieldNameTokens(key) {
|
|
259
|
+
return key
|
|
260
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
|
261
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
262
|
+
.replace(/[^A-Za-z0-9]+/g, "_")
|
|
263
|
+
.toLowerCase()
|
|
264
|
+
.split("_")
|
|
265
|
+
.filter(Boolean);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function hasTokenPair(tokens, left, right) {
|
|
269
|
+
return tokens.some(
|
|
270
|
+
(token, index) => token === left && tokens[index + 1] === right,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function isSensitiveFieldName(key, path, trustedPaths) {
|
|
275
|
+
if (trustedPaths.has([...path, key].join("."))) return false;
|
|
276
|
+
const tokens = fieldNameTokens(key);
|
|
277
|
+
const compact = key.replace(/[^A-Za-z0-9]+/g, "").toLowerCase();
|
|
278
|
+
return (
|
|
279
|
+
/^(?:apikey|apitoken|authorizationheader|authheader|authenticationheader|accesstoken|authtoken|bearertoken|refreshtoken|securitykey|privatekey|signingkey|clientsecret|databasepassword|passwordhash|idtoken|sessiontoken)$/.test(
|
|
280
|
+
compact,
|
|
281
|
+
) ||
|
|
282
|
+
tokens.includes("authorization") ||
|
|
283
|
+
tokens.includes("auth") ||
|
|
284
|
+
tokens.includes("authentication") ||
|
|
285
|
+
tokens.includes("credential") ||
|
|
286
|
+
tokens.includes("credentials") ||
|
|
287
|
+
tokens.includes("password") ||
|
|
288
|
+
tokens.includes("secret") ||
|
|
289
|
+
tokens.includes("token") ||
|
|
290
|
+
hasTokenPair(tokens, "auth", "header") ||
|
|
291
|
+
hasTokenPair(tokens, "authentication", "header") ||
|
|
292
|
+
hasTokenPair(tokens, "api", "key") ||
|
|
293
|
+
hasTokenPair(tokens, "private", "key") ||
|
|
294
|
+
hasTokenPair(tokens, "security", "key") ||
|
|
295
|
+
hasTokenPair(tokens, "signing", "key")
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function redactString(value, sensitiveValues) {
|
|
300
|
+
let redacted = value;
|
|
301
|
+
const uniqueValues = [...new Set(sensitiveValues)]
|
|
302
|
+
.filter((secret) => typeof secret === "string" && secret.length > 0)
|
|
303
|
+
.sort((left, right) => right.length - left.length);
|
|
304
|
+
for (const secret of uniqueValues) {
|
|
305
|
+
redacted = redacted.replaceAll(secret, "[REDACTED]");
|
|
306
|
+
}
|
|
307
|
+
return redacted;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function redactSensitiveValue(value, sensitiveValues, options = {}, path = []) {
|
|
311
|
+
const trustedPaths = options.trustedPaths ?? new Set();
|
|
312
|
+
const trustedObjectPaths = options.trustedObjectPaths ?? new Set();
|
|
313
|
+
if (typeof value === "string") {
|
|
314
|
+
return redactString(value, sensitiveValues);
|
|
315
|
+
}
|
|
316
|
+
if (Array.isArray(value)) {
|
|
317
|
+
return value.map((entry) =>
|
|
318
|
+
redactSensitiveValue(entry, sensitiveValues, options, [...path, "*"]),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
if (value && typeof value === "object") {
|
|
322
|
+
return Object.fromEntries(
|
|
323
|
+
Object.entries(value).map(([key, entry]) => {
|
|
324
|
+
const redactedKey = trustedObjectPaths.has(path.join("."))
|
|
325
|
+
? key
|
|
326
|
+
: redactString(key, sensitiveValues);
|
|
327
|
+
return [
|
|
328
|
+
redactedKey,
|
|
329
|
+
isSensitiveFieldName(key, path, trustedPaths)
|
|
330
|
+
? "[REDACTED]"
|
|
331
|
+
: redactSensitiveValue(entry, sensitiveValues, options, [
|
|
332
|
+
...path,
|
|
333
|
+
key,
|
|
334
|
+
]),
|
|
335
|
+
];
|
|
336
|
+
}),
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function safeResponseDetails(body, sensitiveValues = []) {
|
|
343
|
+
const redacted = redactSensitiveValue(body, sensitiveValues);
|
|
344
|
+
if (typeof redacted === "string") {
|
|
345
|
+
const normalized = redacted.trim();
|
|
346
|
+
return normalized ? normalized.slice(0, 500) : "[response details omitted]";
|
|
347
|
+
}
|
|
348
|
+
if (redacted && typeof redacted === "object" && !Array.isArray(redacted)) {
|
|
349
|
+
const allowed = Object.fromEntries(
|
|
350
|
+
Object.entries(redacted).filter(([key]) => SAFE_ERROR_FIELDS.has(key)),
|
|
351
|
+
);
|
|
352
|
+
if (Object.keys(allowed).length > 0) {
|
|
353
|
+
return redactString(JSON.stringify(allowed), sensitiveValues).slice(
|
|
354
|
+
0,
|
|
355
|
+
500,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return "[response details omitted]";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function safeErrorMessage(error, sensitiveValues = []) {
|
|
363
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
364
|
+
return redactString(message, sensitiveValues);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function safeErrorCause(error, sensitiveValues = []) {
|
|
368
|
+
return new Error(safeErrorMessage(error, sensitiveValues));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function authorizationSensitiveValues(authorizationHeader, securityKey) {
|
|
372
|
+
return [
|
|
373
|
+
authorizationHeader,
|
|
374
|
+
authorizationHeader?.includes(" ")
|
|
375
|
+
? authorizationHeader.slice(authorizationHeader.indexOf(" ") + 1)
|
|
376
|
+
: undefined,
|
|
377
|
+
securityKey,
|
|
378
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function httpError(response, body, sensitiveValues = [], prefix = "HTTP") {
|
|
382
|
+
const statusText = redactString(response.statusText, sensitiveValues);
|
|
383
|
+
return new Error(
|
|
384
|
+
`${prefix} ${response.status} ${statusText}: ${safeResponseDetails(
|
|
385
|
+
body,
|
|
386
|
+
sensitiveValues,
|
|
387
|
+
)}`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function requestJson(url, init, options = {}) {
|
|
392
|
+
const managementAuthorization =
|
|
393
|
+
init?.headers?.Authorization ?? init?.headers?.authorization;
|
|
394
|
+
const sensitiveValues = [
|
|
395
|
+
...(options.sensitiveValues ?? []),
|
|
396
|
+
...authorizationSensitiveValues(managementAuthorization),
|
|
397
|
+
];
|
|
398
|
+
const response = await fetch(url, { ...init, redirect: "error" });
|
|
399
|
+
const body = await parseResponse(response);
|
|
400
|
+
if (!response.ok) {
|
|
401
|
+
throw httpError(response, body, sensitiveValues);
|
|
402
|
+
}
|
|
403
|
+
return body;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function requestSideEffect(url, init, options) {
|
|
407
|
+
const managementAuthorization =
|
|
408
|
+
init?.headers?.Authorization ?? init?.headers?.authorization;
|
|
409
|
+
const sensitiveValues = [
|
|
410
|
+
...(options.sensitiveValues ?? []),
|
|
411
|
+
...authorizationSensitiveValues(managementAuthorization),
|
|
412
|
+
];
|
|
413
|
+
let response;
|
|
414
|
+
const ambiguityGuidance =
|
|
415
|
+
options.ambiguityGuidance ??
|
|
416
|
+
"Reconcile webhook state before retrying; do not blindly retry.";
|
|
417
|
+
try {
|
|
418
|
+
response = await fetch(url, { ...init, redirect: "error" });
|
|
419
|
+
} catch (error) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`${options.operation} status is unknown and may have succeeded. ${ambiguityGuidance}`,
|
|
422
|
+
{ cause: safeErrorCause(error, sensitiveValues) },
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
let body;
|
|
426
|
+
let bodyReadFailed = false;
|
|
427
|
+
try {
|
|
428
|
+
const parsed = await parseResponse(response, { allowOmitted: true });
|
|
429
|
+
body = parsed.body;
|
|
430
|
+
bodyReadFailed = parsed.omitted;
|
|
431
|
+
} catch {
|
|
432
|
+
bodyReadFailed = true;
|
|
433
|
+
}
|
|
434
|
+
if (!response.ok) {
|
|
435
|
+
const error = options.omitResponseBody
|
|
436
|
+
? new Error(`${options.operation} failed with HTTP ${response.status}`)
|
|
437
|
+
: httpError(response, body, sensitiveValues);
|
|
438
|
+
if (DEFINITIVE_MUTATION_REJECTION_STATUSES.has(response.status)) {
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
throw new Error(
|
|
442
|
+
`${safeErrorMessage(error, sensitiveValues)} ${options.operation} status is unknown and may have succeeded. ${ambiguityGuidance}`,
|
|
443
|
+
{ cause: safeErrorCause(error, sensitiveValues) },
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
return { body, bodyReadFailed, response };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function jsonObject(value, message) {
|
|
450
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
451
|
+
throw new Error(message);
|
|
452
|
+
}
|
|
453
|
+
return value;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function webhookFromUnknown(value) {
|
|
457
|
+
const webhook = jsonObject(value, "Webhook response must be an object");
|
|
458
|
+
const id = requireResponseString(
|
|
459
|
+
webhook.id,
|
|
460
|
+
"Webhook response is missing id",
|
|
461
|
+
);
|
|
462
|
+
const webhookUrl = requireResponseString(
|
|
463
|
+
webhook.webhook_url,
|
|
464
|
+
"Webhook response is missing webhook_url",
|
|
465
|
+
);
|
|
466
|
+
if (typeof webhook.requires_authorization_header !== "boolean") {
|
|
467
|
+
throw new Error(
|
|
468
|
+
"Webhook response is missing requires_authorization_header",
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
...webhook,
|
|
473
|
+
id,
|
|
474
|
+
webhook_url: webhookUrl,
|
|
475
|
+
requires_authorization_header: webhook.requires_authorization_header,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function validatedWebhookFromUnknown(value, baseUrl, agentId) {
|
|
480
|
+
const webhook = webhookFromUnknown(value);
|
|
481
|
+
const responseAgentId = requireResponseString(
|
|
482
|
+
webhook.agent_id,
|
|
483
|
+
"Webhook response is missing agent_id",
|
|
484
|
+
);
|
|
485
|
+
if (responseAgentId !== agentId) {
|
|
486
|
+
throw new Error("Webhook response belongs to a different agent");
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
...webhook,
|
|
490
|
+
agent_id: responseAgentId,
|
|
491
|
+
webhook_url: validatedWebhookIngressUrl(webhook, baseUrl),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function webhookList(value, baseUrl, agentId) {
|
|
496
|
+
const body = jsonObject(value, "Webhook list response must be an object");
|
|
497
|
+
if (!Array.isArray(body.webhooks)) {
|
|
498
|
+
throw new Error("Webhook list response is missing webhooks");
|
|
499
|
+
}
|
|
500
|
+
return body.webhooks.map((webhook) =>
|
|
501
|
+
validatedWebhookFromUnknown(webhook, baseUrl, agentId),
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function readSecurityKey(args) {
|
|
506
|
+
if (args["security-key-stdin"] !== true) return undefined;
|
|
507
|
+
process.stdin.setEncoding("utf8");
|
|
508
|
+
let value = "";
|
|
509
|
+
for await (const chunk of process.stdin) value += chunk;
|
|
510
|
+
value = value.replace(/[\r\n]+$/, "");
|
|
511
|
+
if (!value.trim()) {
|
|
512
|
+
throw new Error(
|
|
513
|
+
"Security key from stdin must contain a non-whitespace character",
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return value;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export async function reserveCredentialFile(outputPath, operations = {}) {
|
|
520
|
+
const path = resolve(
|
|
521
|
+
requireString(
|
|
522
|
+
outputPath,
|
|
523
|
+
"Generated credentials require --credential-output <path>",
|
|
524
|
+
),
|
|
525
|
+
);
|
|
526
|
+
const openFile = operations.openFile ?? open;
|
|
527
|
+
const generateRandomBytes = operations.randomBytes ?? randomBytes;
|
|
528
|
+
const unlinkFile = operations.unlinkFile ?? unlink;
|
|
529
|
+
const handle = await openFile(path, "wx", 0o600);
|
|
530
|
+
const reservation = { handle, path };
|
|
531
|
+
let sensitiveValues = [];
|
|
532
|
+
try {
|
|
533
|
+
const securityKey = generateRandomBytes(24).toString("base64url");
|
|
534
|
+
const authorizationHeader =
|
|
535
|
+
buildAgentWebhookBasicAuthorization(securityKey);
|
|
536
|
+
sensitiveValues = authorizationSensitiveValues(
|
|
537
|
+
authorizationHeader,
|
|
538
|
+
securityKey,
|
|
539
|
+
);
|
|
540
|
+
const contents = `${JSON.stringify(
|
|
541
|
+
{
|
|
542
|
+
authorization_header: authorizationHeader,
|
|
543
|
+
security_key: securityKey,
|
|
544
|
+
},
|
|
545
|
+
null,
|
|
546
|
+
2,
|
|
547
|
+
)}\n`;
|
|
548
|
+
await handle.writeFile(contents, "utf8");
|
|
549
|
+
await handle.sync();
|
|
550
|
+
return { ...reservation, authorizationHeader, securityKey };
|
|
551
|
+
} catch (error) {
|
|
552
|
+
throw await cleanupCredentialFile(reservation, error, {
|
|
553
|
+
sensitiveValues,
|
|
554
|
+
unlinkFile,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export async function cleanupCredentialFile(
|
|
560
|
+
reservation,
|
|
561
|
+
originalError,
|
|
562
|
+
operations = {},
|
|
563
|
+
) {
|
|
564
|
+
const unlinkFile = operations.unlinkFile ?? unlink;
|
|
565
|
+
const sensitiveValues = operations.sensitiveValues ?? [];
|
|
566
|
+
const cleanupFailures = [];
|
|
567
|
+
try {
|
|
568
|
+
await reservation.handle.close();
|
|
569
|
+
} catch (error) {
|
|
570
|
+
cleanupFailures.push(`close: ${safeErrorMessage(error, sensitiveValues)}`);
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
await unlinkFile(reservation.path);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
cleanupFailures.push(`unlink: ${safeErrorMessage(error, sensitiveValues)}`);
|
|
576
|
+
}
|
|
577
|
+
const originalMessage = safeErrorMessage(originalError, sensitiveValues);
|
|
578
|
+
const safeCause = safeErrorCause(originalError, sensitiveValues);
|
|
579
|
+
const safePath = redactString(reservation.path, sensitiveValues);
|
|
580
|
+
if (cleanupFailures.length === 0) {
|
|
581
|
+
return new Error(originalMessage, { cause: safeCause });
|
|
582
|
+
}
|
|
583
|
+
return new Error(
|
|
584
|
+
`${originalMessage} Credential cleanup did not complete; a sensitive file may remain at ${safePath}. Remove it manually. Cleanup errors: ${cleanupFailures.join("; ")}`,
|
|
585
|
+
{ cause: safeCause },
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function finishCredentialFile(reservation) {
|
|
590
|
+
await reservation.handle.close();
|
|
591
|
+
return reservation.path;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export async function preserveCredentialFile(
|
|
595
|
+
reservation,
|
|
596
|
+
originalError,
|
|
597
|
+
sensitiveValues,
|
|
598
|
+
) {
|
|
599
|
+
let closeFailure;
|
|
600
|
+
try {
|
|
601
|
+
await reservation.handle.close();
|
|
602
|
+
} catch (error) {
|
|
603
|
+
closeFailure = safeErrorMessage(error, sensitiveValues);
|
|
604
|
+
}
|
|
605
|
+
const suffix = closeFailure
|
|
606
|
+
? ` The file handle did not close cleanly (${closeFailure}); the process will release it on exit.`
|
|
607
|
+
: "";
|
|
608
|
+
const safePath = redactString(reservation.path, sensitiveValues);
|
|
609
|
+
return new Error(
|
|
610
|
+
`${safeErrorMessage(originalError, sensitiveValues)} Webhook creation status is unknown or may have succeeded. The credential was retained at ${safePath}.${suffix} Run list and reconcile the webhook name before retrying; do not create a duplicate.`,
|
|
611
|
+
{ cause: safeErrorCause(originalError, sensitiveValues) },
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function readCredentialAuthorizationHeader(pathValue) {
|
|
616
|
+
const path = resolve(
|
|
617
|
+
requireString(pathValue, "Pass --credential-file <path>"),
|
|
618
|
+
);
|
|
619
|
+
const credentialText = await readFile(path, "utf8");
|
|
620
|
+
let parsedCredential;
|
|
621
|
+
try {
|
|
622
|
+
parsedCredential = JSON.parse(credentialText);
|
|
623
|
+
} catch {
|
|
624
|
+
throw new Error("Credential file contains invalid JSON");
|
|
625
|
+
}
|
|
626
|
+
const credential = jsonObject(
|
|
627
|
+
parsedCredential,
|
|
628
|
+
"Credential file must contain a JSON object",
|
|
629
|
+
);
|
|
630
|
+
const authorizationHeader = requireString(
|
|
631
|
+
credential.authorization_header,
|
|
632
|
+
"Credential file is missing authorization_header",
|
|
633
|
+
);
|
|
634
|
+
const securityKey =
|
|
635
|
+
credential.security_key === undefined
|
|
636
|
+
? undefined
|
|
637
|
+
: requireString(
|
|
638
|
+
credential.security_key,
|
|
639
|
+
"Credential file security_key must be non-empty",
|
|
640
|
+
);
|
|
641
|
+
if (
|
|
642
|
+
securityKey &&
|
|
643
|
+
authorizationHeader !== buildAgentWebhookBasicAuthorization(securityKey)
|
|
644
|
+
) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
"Credential file authorization_header does not match security_key",
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
authorizationHeader,
|
|
651
|
+
sensitiveValues: authorizationSensitiveValues(
|
|
652
|
+
authorizationHeader,
|
|
653
|
+
securityKey,
|
|
654
|
+
),
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function readPayload(args) {
|
|
659
|
+
if (args["payload-file"]) {
|
|
660
|
+
const payloadText = await readFile(String(args["payload-file"]), "utf8");
|
|
661
|
+
try {
|
|
662
|
+
return JSON.parse(payloadText);
|
|
663
|
+
} catch {
|
|
664
|
+
throw new Error("Payload file contains invalid JSON");
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
event: "agent-webhook-test",
|
|
669
|
+
message: "Test delivery from Letta Code",
|
|
670
|
+
sent_at: new Date().toISOString(),
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function positiveInteger(value, fallback) {
|
|
675
|
+
if (value === undefined) return fallback;
|
|
676
|
+
const parsed = Number(value);
|
|
677
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 50) {
|
|
678
|
+
throw new Error("--limit must be an integer from 1 through 50");
|
|
679
|
+
}
|
|
680
|
+
return parsed;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const DEFINITIVE_MUTATION_REJECTION_STATUSES = new Set([
|
|
684
|
+
400, 401, 403, 404, 405, 413, 415, 422,
|
|
685
|
+
]);
|
|
686
|
+
|
|
687
|
+
function unknownCreationError(originalError, sensitiveValues) {
|
|
688
|
+
return new Error(
|
|
689
|
+
`${safeErrorMessage(originalError, sensitiveValues)} Webhook creation status is unknown or may have succeeded. Run list and reconcile the webhook name before retrying; do not create a duplicate.`,
|
|
690
|
+
{ cause: safeErrorCause(originalError, sensitiveValues) },
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function managementHeaders(apiKey) {
|
|
695
|
+
return {
|
|
696
|
+
Accept: "application/json",
|
|
697
|
+
Authorization: `Bearer ${apiKey}`,
|
|
698
|
+
"Content-Type": "application/json",
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function serializeSanitizedJson(value, sensitiveValues, options) {
|
|
703
|
+
const sanitized = redactSensitiveValue(value, sensitiveValues, options);
|
|
704
|
+
return JSON.stringify(sanitized, null, 2);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function printJson(value, sensitiveValues = [], options = {}) {
|
|
708
|
+
const serialized = serializeSanitizedJson(value, sensitiveValues, options);
|
|
709
|
+
if (Buffer.byteLength(serialized, "utf8") + 1 <= 1024 * 1024) {
|
|
710
|
+
console.log(serialized);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const oversizedFallback = options.oversizedFallback ?? {
|
|
714
|
+
output_omitted: true,
|
|
715
|
+
reason: "Sanitized output exceeded the 1 MiB display limit",
|
|
716
|
+
};
|
|
717
|
+
const fallback = serializeSanitizedJson(
|
|
718
|
+
oversizedFallback,
|
|
719
|
+
sensitiveValues,
|
|
720
|
+
options,
|
|
721
|
+
);
|
|
722
|
+
if (Buffer.byteLength(fallback, "utf8") + 1 > 1024 * 1024) {
|
|
723
|
+
console.log(
|
|
724
|
+
'{\n "output_omitted": true,\n "reason": "Sanitized output exceeded the 1 MiB display limit"\n}',
|
|
725
|
+
);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
console.log(fallback);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const CREATE_OUTPUT_TRUSTED_PATHS = new Set([
|
|
732
|
+
"credential_file",
|
|
733
|
+
"credential_file_is_sensitive",
|
|
734
|
+
"credential_is_recoverable_from_server",
|
|
735
|
+
"webhook.requires_authorization_header",
|
|
736
|
+
]);
|
|
737
|
+
const CREATE_OUTPUT_TRUSTED_OBJECT_PATHS = new Set(["", "webhook"]);
|
|
738
|
+
const LIST_OUTPUT_TRUSTED_PATHS = new Set([
|
|
739
|
+
"webhooks.*.requires_authorization_header",
|
|
740
|
+
]);
|
|
741
|
+
const LIST_OUTPUT_TRUSTED_OBJECT_PATHS = new Set(["", "webhooks.*"]);
|
|
742
|
+
const REQUEST_OUTPUT_TRUSTED_PATHS = new Set([
|
|
743
|
+
"requests.*.authorization_passed",
|
|
744
|
+
]);
|
|
745
|
+
const REQUEST_OUTPUT_TRUSTED_OBJECT_PATHS = new Set(["", "requests.*"]);
|
|
746
|
+
const WEBHOOK_OUTPUT_TRUSTED_PATHS = new Set([
|
|
747
|
+
"webhook.requires_authorization_header",
|
|
748
|
+
]);
|
|
749
|
+
const WEBHOOK_OUTPUT_TRUSTED_OBJECT_PATHS = new Set(["", "webhook"]);
|
|
750
|
+
const ROOT_OUTPUT_TRUSTED_OBJECT_PATHS = new Set([""]);
|
|
751
|
+
|
|
752
|
+
function requireWebhookId(args) {
|
|
753
|
+
return requireString(args["webhook-id"], "Pass --webhook-id");
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function requireConfirmation(args, action) {
|
|
757
|
+
if (args.confirm !== true) {
|
|
758
|
+
throw new Error(`${action} requires --confirm`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function listWebhooks(params) {
|
|
763
|
+
const encodedAgentId = encodeURIComponent(params.agentId);
|
|
764
|
+
return webhookList(
|
|
765
|
+
await requestJson(
|
|
766
|
+
`${params.baseUrl}/v1/agents/${encodedAgentId}/webhooks`,
|
|
767
|
+
{
|
|
768
|
+
headers: params.headers,
|
|
769
|
+
},
|
|
770
|
+
),
|
|
771
|
+
params.baseUrl,
|
|
772
|
+
params.agentId,
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async function runManagementCommand(params) {
|
|
777
|
+
const encodedAgentId = encodeURIComponent(params.agentId);
|
|
778
|
+
const collectionUrl = `${params.baseUrl}/v1/agents/${encodedAgentId}/webhooks`;
|
|
779
|
+
const managementSensitiveValues = authorizationSensitiveValues(
|
|
780
|
+
params.headers.Authorization,
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
if (params.command === "list") {
|
|
784
|
+
const webhooks = await listWebhooks(params);
|
|
785
|
+
printJson(
|
|
786
|
+
{ webhooks: webhooks.map(safeAgentWebhook) },
|
|
787
|
+
managementSensitiveValues,
|
|
788
|
+
{
|
|
789
|
+
oversizedFallback: {
|
|
790
|
+
output_omitted: true,
|
|
791
|
+
reason: "Webhook list exceeded the 1 MiB display limit",
|
|
792
|
+
webhook_count: webhooks.length,
|
|
793
|
+
},
|
|
794
|
+
trustedObjectPaths: LIST_OUTPUT_TRUSTED_OBJECT_PATHS,
|
|
795
|
+
trustedPaths: LIST_OUTPUT_TRUSTED_PATHS,
|
|
796
|
+
},
|
|
797
|
+
);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (params.command === "create") {
|
|
802
|
+
const isPublic = params.args.public === true;
|
|
803
|
+
const usesGeneratedCredential = params.args.secure === true;
|
|
804
|
+
const usesSecurityKey = params.args["security-key-stdin"] === true;
|
|
805
|
+
const modeCount = [
|
|
806
|
+
isPublic,
|
|
807
|
+
usesGeneratedCredential,
|
|
808
|
+
usesSecurityKey,
|
|
809
|
+
].filter(Boolean).length;
|
|
810
|
+
if (modeCount !== 1) {
|
|
811
|
+
throw new Error(
|
|
812
|
+
"Choose exactly one of --public, --secure, or --security-key-stdin",
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
if (!usesGeneratedCredential && params.args["credential-output"]) {
|
|
816
|
+
throw new Error("--credential-output is valid only with --secure");
|
|
817
|
+
}
|
|
818
|
+
const securityKey = await readSecurityKey(params.args);
|
|
819
|
+
const body = {
|
|
820
|
+
enabled: params.args.disabled !== true,
|
|
821
|
+
name: requireString(params.args.name, "Create requires --name"),
|
|
822
|
+
requires_authorization_header: usesGeneratedCredential || usesSecurityKey,
|
|
823
|
+
};
|
|
824
|
+
if (params.args.preprompt !== undefined) {
|
|
825
|
+
body.preprompt = requireString(
|
|
826
|
+
params.args.preprompt,
|
|
827
|
+
"--preprompt must contain a non-whitespace character",
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
if (securityKey) body.security_key = securityKey;
|
|
831
|
+
const credentialReservation = usesGeneratedCredential
|
|
832
|
+
? await reserveCredentialFile(params.args["credential-output"])
|
|
833
|
+
: undefined;
|
|
834
|
+
if (credentialReservation) {
|
|
835
|
+
body.security_key = credentialReservation.securityKey;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const sensitiveValues = [
|
|
839
|
+
...authorizationSensitiveValues(params.headers.Authorization),
|
|
840
|
+
...(credentialReservation
|
|
841
|
+
? authorizationSensitiveValues(
|
|
842
|
+
credentialReservation.authorizationHeader,
|
|
843
|
+
credentialReservation.securityKey,
|
|
844
|
+
)
|
|
845
|
+
: securityKey
|
|
846
|
+
? authorizationSensitiveValues(
|
|
847
|
+
buildAgentWebhookBasicAuthorization(securityKey),
|
|
848
|
+
securityKey,
|
|
849
|
+
)
|
|
850
|
+
: []),
|
|
851
|
+
];
|
|
852
|
+
let creationState = "before-dispatch";
|
|
853
|
+
try {
|
|
854
|
+
creationState = "dispatched";
|
|
855
|
+
const rawResponse = await fetch(collectionUrl, {
|
|
856
|
+
body: JSON.stringify(body),
|
|
857
|
+
headers: params.headers,
|
|
858
|
+
method: "POST",
|
|
859
|
+
redirect: "error",
|
|
860
|
+
});
|
|
861
|
+
if (rawResponse.ok) {
|
|
862
|
+
creationState = "accepted";
|
|
863
|
+
} else if (
|
|
864
|
+
DEFINITIVE_MUTATION_REJECTION_STATUSES.has(rawResponse.status)
|
|
865
|
+
) {
|
|
866
|
+
creationState = "definitive-rejection";
|
|
867
|
+
}
|
|
868
|
+
const responseBody = await parseResponse(rawResponse);
|
|
869
|
+
if (!rawResponse.ok) {
|
|
870
|
+
throw httpError(rawResponse, responseBody, sensitiveValues);
|
|
871
|
+
}
|
|
872
|
+
const response = jsonObject(
|
|
873
|
+
responseBody,
|
|
874
|
+
"Create response must be an object",
|
|
875
|
+
);
|
|
876
|
+
const webhook = validatedWebhookFromUnknown(
|
|
877
|
+
response.webhook,
|
|
878
|
+
params.baseUrl,
|
|
879
|
+
params.agentId,
|
|
880
|
+
);
|
|
881
|
+
const requestedAuthorization = usesGeneratedCredential || usesSecurityKey;
|
|
882
|
+
if (webhook.requires_authorization_header !== requestedAuthorization) {
|
|
883
|
+
throw new Error(
|
|
884
|
+
"Create response authentication mode does not match the request",
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
const expectedAuthorizationHeader = credentialReservation
|
|
888
|
+
? credentialReservation.authorizationHeader
|
|
889
|
+
: securityKey
|
|
890
|
+
? buildAgentWebhookBasicAuthorization(securityKey)
|
|
891
|
+
: undefined;
|
|
892
|
+
if (
|
|
893
|
+
expectedAuthorizationHeader &&
|
|
894
|
+
webhook.authorization_header !== expectedAuthorizationHeader
|
|
895
|
+
) {
|
|
896
|
+
throw new Error(
|
|
897
|
+
"Create response authorization_header does not match the requested credential",
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
if (
|
|
901
|
+
!expectedAuthorizationHeader &&
|
|
902
|
+
webhook.authorization_header !== null &&
|
|
903
|
+
webhook.authorization_header !== undefined
|
|
904
|
+
) {
|
|
905
|
+
throw new Error(
|
|
906
|
+
"Create response unexpectedly returned a credential for a public webhook",
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
const prepromptMatches =
|
|
910
|
+
body.preprompt === undefined
|
|
911
|
+
? typeof webhook.preprompt === "string" &&
|
|
912
|
+
webhook.preprompt.trim().length > 0
|
|
913
|
+
: webhook.preprompt === body.preprompt;
|
|
914
|
+
if (
|
|
915
|
+
webhook.name !== body.name ||
|
|
916
|
+
webhook.enabled !== body.enabled ||
|
|
917
|
+
!prepromptMatches
|
|
918
|
+
) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
"Create response does not match the requested webhook configuration",
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
const credentialFile = credentialReservation
|
|
924
|
+
? await finishCredentialFile(credentialReservation)
|
|
925
|
+
: undefined;
|
|
926
|
+
printJson(
|
|
927
|
+
{
|
|
928
|
+
...(credentialFile
|
|
929
|
+
? {
|
|
930
|
+
credential_file: credentialFile,
|
|
931
|
+
credential_file_is_sensitive: true,
|
|
932
|
+
credential_is_recoverable_from_server: false,
|
|
933
|
+
}
|
|
934
|
+
: {}),
|
|
935
|
+
webhook: safeAgentWebhook(webhook),
|
|
936
|
+
},
|
|
937
|
+
sensitiveValues,
|
|
938
|
+
{
|
|
939
|
+
oversizedFallback: {
|
|
940
|
+
created: true,
|
|
941
|
+
credential_file: credentialFile,
|
|
942
|
+
output_omitted: true,
|
|
943
|
+
reason: "Create output exceeded the 1 MiB display limit",
|
|
944
|
+
webhook_id: webhook.id,
|
|
945
|
+
webhook_url: webhook.webhook_url,
|
|
946
|
+
},
|
|
947
|
+
trustedObjectPaths: CREATE_OUTPUT_TRUSTED_OBJECT_PATHS,
|
|
948
|
+
trustedPaths: CREATE_OUTPUT_TRUSTED_PATHS,
|
|
949
|
+
},
|
|
950
|
+
);
|
|
951
|
+
} catch (error) {
|
|
952
|
+
if (credentialReservation) {
|
|
953
|
+
if (
|
|
954
|
+
creationState === "before-dispatch" ||
|
|
955
|
+
creationState === "definitive-rejection"
|
|
956
|
+
) {
|
|
957
|
+
throw await cleanupCredentialFile(credentialReservation, error, {
|
|
958
|
+
sensitiveValues,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
throw await preserveCredentialFile(
|
|
962
|
+
credentialReservation,
|
|
963
|
+
error,
|
|
964
|
+
sensitiveValues,
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
if (creationState === "dispatched" || creationState === "accepted") {
|
|
968
|
+
throw unknownCreationError(error, sensitiveValues);
|
|
969
|
+
}
|
|
970
|
+
throw new Error(safeErrorMessage(error, sensitiveValues), {
|
|
971
|
+
cause: safeErrorCause(error, sensitiveValues),
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const webhookId = requireWebhookId(params.args);
|
|
978
|
+
const itemUrl = `${collectionUrl}/${encodeURIComponent(webhookId)}`;
|
|
979
|
+
|
|
980
|
+
if (params.command === "test") {
|
|
981
|
+
requireConfirmation(params.args, "Testing a webhook");
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (params.command === "requests") {
|
|
985
|
+
const limit = positiveInteger(params.args.limit, 10);
|
|
986
|
+
const response = jsonObject(
|
|
987
|
+
await requestJson(`${itemUrl}/requests?limit=${limit}`, {
|
|
988
|
+
headers: params.headers,
|
|
989
|
+
}),
|
|
990
|
+
"Request history response must be an object",
|
|
991
|
+
);
|
|
992
|
+
if (!Array.isArray(response.requests)) {
|
|
993
|
+
throw new Error("Request history response is missing requests");
|
|
994
|
+
}
|
|
995
|
+
const requests = response.requests.map((entry) => {
|
|
996
|
+
const request = jsonObject(
|
|
997
|
+
entry,
|
|
998
|
+
"Request history item must be an object",
|
|
999
|
+
);
|
|
1000
|
+
const id = requireResponseString(
|
|
1001
|
+
request.id,
|
|
1002
|
+
"Request history item id must be a nonblank string",
|
|
1003
|
+
);
|
|
1004
|
+
if (!Number.isInteger(request.status_code)) {
|
|
1005
|
+
throw new Error("Request history item status_code must be an integer");
|
|
1006
|
+
}
|
|
1007
|
+
if (typeof request.authorization_passed !== "boolean") {
|
|
1008
|
+
throw new Error(
|
|
1009
|
+
"Request history item authorization_passed must be a boolean",
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
if (typeof request.enqueued !== "boolean") {
|
|
1013
|
+
throw new Error("Request history item enqueued must be a boolean");
|
|
1014
|
+
}
|
|
1015
|
+
if (
|
|
1016
|
+
request.error_message !== null &&
|
|
1017
|
+
typeof request.error_message !== "string"
|
|
1018
|
+
) {
|
|
1019
|
+
throw new Error(
|
|
1020
|
+
"Request history item error_message must be a string or null",
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
const createdAt = requireResponseString(
|
|
1024
|
+
request.created_at,
|
|
1025
|
+
"Request history item created_at must be a nonblank string",
|
|
1026
|
+
);
|
|
1027
|
+
return {
|
|
1028
|
+
authorization_passed: request.authorization_passed,
|
|
1029
|
+
accepted: request.status_code === 202,
|
|
1030
|
+
created_at: createdAt,
|
|
1031
|
+
error_message: request.error_message,
|
|
1032
|
+
id,
|
|
1033
|
+
status_code: request.status_code,
|
|
1034
|
+
...(params.args["include-body"] === true
|
|
1035
|
+
? { request_body: request.request_body }
|
|
1036
|
+
: {}),
|
|
1037
|
+
};
|
|
1038
|
+
});
|
|
1039
|
+
printJson({ requests }, managementSensitiveValues, {
|
|
1040
|
+
oversizedFallback: {
|
|
1041
|
+
output_omitted: true,
|
|
1042
|
+
reason: "Request history exceeded the 1 MiB display limit",
|
|
1043
|
+
request_count: requests.length,
|
|
1044
|
+
},
|
|
1045
|
+
trustedObjectPaths: REQUEST_OUTPUT_TRUSTED_OBJECT_PATHS,
|
|
1046
|
+
trustedPaths: REQUEST_OUTPUT_TRUSTED_PATHS,
|
|
1047
|
+
});
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
if (params.command === "enable" || params.command === "disable") {
|
|
1052
|
+
if (params.command === "disable") {
|
|
1053
|
+
requireConfirmation(params.args, "Disabling a webhook");
|
|
1054
|
+
}
|
|
1055
|
+
const mutation = await requestSideEffect(
|
|
1056
|
+
itemUrl,
|
|
1057
|
+
{
|
|
1058
|
+
body: JSON.stringify({ enabled: params.command === "enable" }),
|
|
1059
|
+
headers: params.headers,
|
|
1060
|
+
method: "PATCH",
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
operation: `Webhook ${params.command}`,
|
|
1064
|
+
},
|
|
1065
|
+
);
|
|
1066
|
+
let webhook;
|
|
1067
|
+
try {
|
|
1068
|
+
const response = jsonObject(
|
|
1069
|
+
mutation.body,
|
|
1070
|
+
"Update response must be an object",
|
|
1071
|
+
);
|
|
1072
|
+
webhook = validatedWebhookFromUnknown(
|
|
1073
|
+
response.webhook,
|
|
1074
|
+
params.baseUrl,
|
|
1075
|
+
params.agentId,
|
|
1076
|
+
);
|
|
1077
|
+
const expectedEnabled = params.command === "enable";
|
|
1078
|
+
if (
|
|
1079
|
+
webhook.id !== webhookId ||
|
|
1080
|
+
typeof webhook.enabled !== "boolean" ||
|
|
1081
|
+
webhook.enabled !== expectedEnabled
|
|
1082
|
+
) {
|
|
1083
|
+
throw new Error(
|
|
1084
|
+
"Update response does not match the requested webhook state",
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
} catch {
|
|
1088
|
+
printJson(
|
|
1089
|
+
{
|
|
1090
|
+
accepted: true,
|
|
1091
|
+
body_omitted: true,
|
|
1092
|
+
operation: params.command,
|
|
1093
|
+
reason:
|
|
1094
|
+
"Successful update response could not be validated; list webhooks to reconcile before retrying",
|
|
1095
|
+
status_code: mutation.response.status,
|
|
1096
|
+
webhook_id: webhookId,
|
|
1097
|
+
},
|
|
1098
|
+
[],
|
|
1099
|
+
{ trustedObjectPaths: ROOT_OUTPUT_TRUSTED_OBJECT_PATHS },
|
|
1100
|
+
);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
printJson(
|
|
1104
|
+
{
|
|
1105
|
+
webhook: safeAgentWebhook(webhook),
|
|
1106
|
+
},
|
|
1107
|
+
managementSensitiveValues,
|
|
1108
|
+
{
|
|
1109
|
+
oversizedFallback: {
|
|
1110
|
+
output_omitted: true,
|
|
1111
|
+
reason: "Update output exceeded the 1 MiB display limit",
|
|
1112
|
+
updated: true,
|
|
1113
|
+
webhook_id: webhookId,
|
|
1114
|
+
},
|
|
1115
|
+
trustedObjectPaths: WEBHOOK_OUTPUT_TRUSTED_OBJECT_PATHS,
|
|
1116
|
+
trustedPaths: WEBHOOK_OUTPUT_TRUSTED_PATHS,
|
|
1117
|
+
},
|
|
1118
|
+
);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (params.command === "rotate") {
|
|
1123
|
+
requireConfirmation(params.args, "Rotating a webhook URL");
|
|
1124
|
+
const existingWebhook = (await listWebhooks(params)).find(
|
|
1125
|
+
(candidate) => candidate.id === webhookId,
|
|
1126
|
+
);
|
|
1127
|
+
if (!existingWebhook) throw new Error(`Webhook not found: ${webhookId}`);
|
|
1128
|
+
const mutation = await requestSideEffect(
|
|
1129
|
+
`${itemUrl}/refresh`,
|
|
1130
|
+
{
|
|
1131
|
+
body: "{}",
|
|
1132
|
+
headers: params.headers,
|
|
1133
|
+
method: "POST",
|
|
1134
|
+
},
|
|
1135
|
+
{ operation: "Webhook rotation" },
|
|
1136
|
+
);
|
|
1137
|
+
let webhook;
|
|
1138
|
+
try {
|
|
1139
|
+
const response = jsonObject(
|
|
1140
|
+
mutation.body,
|
|
1141
|
+
"Rotate response must be an object",
|
|
1142
|
+
);
|
|
1143
|
+
webhook = validatedWebhookFromUnknown(
|
|
1144
|
+
response.webhook,
|
|
1145
|
+
params.baseUrl,
|
|
1146
|
+
params.agentId,
|
|
1147
|
+
);
|
|
1148
|
+
if (
|
|
1149
|
+
webhook.id !== webhookId ||
|
|
1150
|
+
webhook.webhook_url === existingWebhook.webhook_url ||
|
|
1151
|
+
webhook.webhook_slug === existingWebhook.webhook_slug
|
|
1152
|
+
) {
|
|
1153
|
+
throw new Error(
|
|
1154
|
+
"Rotate response does not prove the requested webhook URL changed",
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
} catch {
|
|
1158
|
+
printJson(
|
|
1159
|
+
{
|
|
1160
|
+
accepted: true,
|
|
1161
|
+
body_omitted: true,
|
|
1162
|
+
operation: "rotate",
|
|
1163
|
+
reason:
|
|
1164
|
+
"Successful rotate response could not be validated; list webhooks to reconcile before retrying",
|
|
1165
|
+
status_code: mutation.response.status,
|
|
1166
|
+
webhook_id: webhookId,
|
|
1167
|
+
},
|
|
1168
|
+
[],
|
|
1169
|
+
{ trustedObjectPaths: ROOT_OUTPUT_TRUSTED_OBJECT_PATHS },
|
|
1170
|
+
);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
printJson(
|
|
1174
|
+
{
|
|
1175
|
+
webhook: safeAgentWebhook(webhook),
|
|
1176
|
+
},
|
|
1177
|
+
managementSensitiveValues,
|
|
1178
|
+
{
|
|
1179
|
+
oversizedFallback: {
|
|
1180
|
+
output_omitted: true,
|
|
1181
|
+
reason: "Rotate output exceeded the 1 MiB display limit",
|
|
1182
|
+
rotated: true,
|
|
1183
|
+
webhook_id: webhookId,
|
|
1184
|
+
},
|
|
1185
|
+
trustedObjectPaths: WEBHOOK_OUTPUT_TRUSTED_OBJECT_PATHS,
|
|
1186
|
+
trustedPaths: WEBHOOK_OUTPUT_TRUSTED_PATHS,
|
|
1187
|
+
},
|
|
1188
|
+
);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
if (params.command === "delete") {
|
|
1193
|
+
requireConfirmation(params.args, "Deleting a webhook");
|
|
1194
|
+
const mutation = await requestSideEffect(
|
|
1195
|
+
itemUrl,
|
|
1196
|
+
{
|
|
1197
|
+
body: "{}",
|
|
1198
|
+
headers: params.headers,
|
|
1199
|
+
method: "DELETE",
|
|
1200
|
+
},
|
|
1201
|
+
{ operation: "Webhook deletion" },
|
|
1202
|
+
);
|
|
1203
|
+
let deletionConfirmed = false;
|
|
1204
|
+
if (!mutation.bodyReadFailed && mutation.response.status === 200) {
|
|
1205
|
+
try {
|
|
1206
|
+
const response = jsonObject(
|
|
1207
|
+
mutation.body,
|
|
1208
|
+
"Delete response must be an object",
|
|
1209
|
+
);
|
|
1210
|
+
deletionConfirmed = response.success === true;
|
|
1211
|
+
} catch {
|
|
1212
|
+
deletionConfirmed = false;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
printJson(
|
|
1216
|
+
deletionConfirmed
|
|
1217
|
+
? {
|
|
1218
|
+
deleted: true,
|
|
1219
|
+
status_code: mutation.response.status,
|
|
1220
|
+
webhook_id: webhookId,
|
|
1221
|
+
}
|
|
1222
|
+
: {
|
|
1223
|
+
accepted: true,
|
|
1224
|
+
body_omitted: true,
|
|
1225
|
+
deletion_confirmed: false,
|
|
1226
|
+
operation: "delete",
|
|
1227
|
+
reason:
|
|
1228
|
+
"Successful delete response did not confirm deletion; list webhooks before treating the capability as removed or retrying",
|
|
1229
|
+
status_code: mutation.response.status,
|
|
1230
|
+
webhook_id: webhookId,
|
|
1231
|
+
},
|
|
1232
|
+
[],
|
|
1233
|
+
{ trustedObjectPaths: ROOT_OUTPUT_TRUSTED_OBJECT_PATHS },
|
|
1234
|
+
);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
if (params.command === "test") {
|
|
1239
|
+
const webhooks = await listWebhooks(params);
|
|
1240
|
+
const webhook = webhooks.find((candidate) => candidate.id === webhookId);
|
|
1241
|
+
if (!webhook) throw new Error(`Webhook not found: ${webhookId}`);
|
|
1242
|
+
const usesCredentialFile = params.args["credential-file"] !== undefined;
|
|
1243
|
+
const usesSecurityKey = params.args["security-key-stdin"] === true;
|
|
1244
|
+
if (usesCredentialFile && usesSecurityKey) {
|
|
1245
|
+
throw new Error(
|
|
1246
|
+
"Choose at most one of --credential-file or --security-key-stdin",
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
const securityKey = await readSecurityKey(params.args);
|
|
1250
|
+
const credential = usesCredentialFile
|
|
1251
|
+
? await readCredentialAuthorizationHeader(params.args["credential-file"])
|
|
1252
|
+
: undefined;
|
|
1253
|
+
const authorizationHeader = credential
|
|
1254
|
+
? credential.authorizationHeader
|
|
1255
|
+
: securityKey
|
|
1256
|
+
? buildAgentWebhookBasicAuthorization(securityKey)
|
|
1257
|
+
: undefined;
|
|
1258
|
+
const sensitiveValues = credential
|
|
1259
|
+
? credential.sensitiveValues
|
|
1260
|
+
: authorizationHeader
|
|
1261
|
+
? authorizationSensitiveValues(authorizationHeader, securityKey)
|
|
1262
|
+
: [];
|
|
1263
|
+
if (webhook.requires_authorization_header && !authorizationHeader) {
|
|
1264
|
+
throw new Error(
|
|
1265
|
+
"This webhook requires --credential-file or --security-key-stdin",
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
if (!webhook.requires_authorization_header && authorizationHeader) {
|
|
1269
|
+
throw new Error("This webhook is public; do not supply credentials");
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const headers = {
|
|
1273
|
+
Accept: "application/json",
|
|
1274
|
+
"Content-Type": "application/json",
|
|
1275
|
+
};
|
|
1276
|
+
if (authorizationHeader) {
|
|
1277
|
+
headers.Authorization = authorizationHeader;
|
|
1278
|
+
}
|
|
1279
|
+
const ingressUrl = validatedWebhookIngressUrl(webhook, params.baseUrl);
|
|
1280
|
+
const delivery = await requestSideEffect(
|
|
1281
|
+
ingressUrl,
|
|
1282
|
+
{
|
|
1283
|
+
body: JSON.stringify(await readPayload(params.args)),
|
|
1284
|
+
headers,
|
|
1285
|
+
method: "POST",
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
ambiguityGuidance:
|
|
1289
|
+
"Delivery may already have triggered agent work, and request history cannot reliably reconcile asynchronous dispatch. Do not retry without renewed affirmative user consent and explicit acceptance of duplicate work risk.",
|
|
1290
|
+
operation: "Webhook test delivery",
|
|
1291
|
+
omitResponseBody: true,
|
|
1292
|
+
sensitiveValues,
|
|
1293
|
+
},
|
|
1294
|
+
);
|
|
1295
|
+
if (delivery.bodyReadFailed) {
|
|
1296
|
+
printJson(
|
|
1297
|
+
{
|
|
1298
|
+
accepted: true,
|
|
1299
|
+
accepted_for_processing: delivery.response.status === 202,
|
|
1300
|
+
body_omitted: true,
|
|
1301
|
+
dispatch_verified: false,
|
|
1302
|
+
note: "The ingress endpoint accepted the request, but its response body could not be read. Request history cannot reliably reconcile asynchronous dispatch. Do not retry without renewed affirmative user consent and explicit acceptance of duplicate work risk.",
|
|
1303
|
+
status_code: delivery.response.status,
|
|
1304
|
+
},
|
|
1305
|
+
[],
|
|
1306
|
+
{ trustedObjectPaths: ROOT_OUTPUT_TRUSTED_OBJECT_PATHS },
|
|
1307
|
+
);
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
printJson(
|
|
1311
|
+
{
|
|
1312
|
+
accepted_for_processing: delivery.response.status === 202,
|
|
1313
|
+
body_omitted: true,
|
|
1314
|
+
dispatch_verified: false,
|
|
1315
|
+
note: "HTTP 202 confirms only that the ingress handler accepted the request; the untrusted response body is omitted, and conversation creation and queue submission happen asynchronously.",
|
|
1316
|
+
status_code: delivery.response.status,
|
|
1317
|
+
},
|
|
1318
|
+
sensitiveValues,
|
|
1319
|
+
{
|
|
1320
|
+
oversizedFallback: {
|
|
1321
|
+
accepted_for_processing: delivery.response.status === 202,
|
|
1322
|
+
body_omitted: true,
|
|
1323
|
+
dispatch_verified: false,
|
|
1324
|
+
note: "HTTP 202 confirms only that the ingress handler accepted the request; the untrusted response body is omitted, and conversation creation and queue submission happen asynchronously.",
|
|
1325
|
+
reason: "Webhook response body exceeded the 1 MiB display limit",
|
|
1326
|
+
status_code: delivery.response.status,
|
|
1327
|
+
},
|
|
1328
|
+
trustedObjectPaths: ROOT_OUTPUT_TRUSTED_OBJECT_PATHS,
|
|
1329
|
+
},
|
|
1330
|
+
);
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
throw new Error(`Unsupported command: ${params.command}`);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
export async function runAgentWebhookCli(argv) {
|
|
1338
|
+
const { args, command } = parseAgentWebhookArgs(argv);
|
|
1339
|
+
const agentId = resolveAgentWebhookTarget(process.env.AGENT_ID);
|
|
1340
|
+
const baseUrl = normalizedBaseUrl(process.env.LETTA_BASE_URL);
|
|
1341
|
+
const apiKey = requireString(
|
|
1342
|
+
process.env.LETTA_API_KEY,
|
|
1343
|
+
"LETTA_API_KEY is required",
|
|
1344
|
+
);
|
|
1345
|
+
await runManagementCommand({
|
|
1346
|
+
agentId,
|
|
1347
|
+
args,
|
|
1348
|
+
baseUrl,
|
|
1349
|
+
command,
|
|
1350
|
+
headers: managementHeaders(apiKey),
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
const isMain =
|
|
1355
|
+
process.argv[1] !== undefined &&
|
|
1356
|
+
realpathSync(fileURLToPath(import.meta.url)) ===
|
|
1357
|
+
realpathSync(resolve(process.argv[1]));
|
|
1358
|
+
|
|
1359
|
+
if (isMain) {
|
|
1360
|
+
runAgentWebhookCli(process.argv.slice(2)).catch((error) => {
|
|
1361
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1362
|
+
process.exit(1);
|
|
1363
|
+
});
|
|
1364
|
+
}
|