aamp-openclaw-plugin 0.1.38 → 0.1.40

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/index.js CHANGED
@@ -8,7 +8,6 @@ var AAMP_HEADER = {
8
8
  INTENT: "X-AAMP-Intent",
9
9
  TASK_ID: "X-AAMP-TaskId",
10
10
  SESSION_KEY: "X-AAMP-Session-Key",
11
- CONTEXT_LINKS: "X-AAMP-ContextLinks",
12
11
  DISPATCH_CONTEXT: "X-AAMP-Dispatch-Context",
13
12
  PRIORITY: "X-AAMP-Priority",
14
13
  EXPIRES_AT: "X-AAMP-Expires-At",
@@ -20,6 +19,8 @@ var AAMP_HEADER = {
20
19
  BLOCKED_REASON: "X-AAMP-BlockedReason",
21
20
  SUGGESTED_OPTIONS: "X-AAMP-SuggestedOptions",
22
21
  STREAM_ID: "X-AAMP-Stream-Id",
22
+ PAIR_CODE: "X-AAMP-Pair-Code",
23
+ DISPATCH_CONTEXT_RULES: "X-AAMP-Dispatch-Context-Rules",
23
24
  PARENT_TASK_ID: "X-AAMP-ParentTaskId",
24
25
  CARD_SUMMARY: "X-AAMP-Card-Summary"
25
26
  };
@@ -159,6 +160,20 @@ function encodeStructuredResult(value) {
159
160
  const json = JSON.stringify(value);
160
161
  return Buffer.from(json, "utf-8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
161
162
  }
163
+ function decodeBase64UrlJson(value) {
164
+ if (!value)
165
+ return void 0;
166
+ try {
167
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
168
+ const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
169
+ return JSON.parse(Buffer.from(normalized + padding, "base64").toString("utf-8"));
170
+ } catch {
171
+ return void 0;
172
+ }
173
+ }
174
+ function encodeBase64UrlJson(value) {
175
+ return Buffer.from(JSON.stringify(value), "utf-8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
176
+ }
162
177
  function parseAampHeaders(meta) {
163
178
  const headers = normalizeHeaders(meta.headers);
164
179
  const intent = getAampHeader(headers, AAMP_HEADER.INTENT);
@@ -170,7 +185,6 @@ function parseAampHeaders(meta) {
170
185
  const to = meta.to.replace(/^<|>$/g, "");
171
186
  const decodedSubject = decodeMimeEncodedWords(meta.subject);
172
187
  if (intent === "task.dispatch") {
173
- const contextLinksStr = getAampHeader(headers, AAMP_HEADER.CONTEXT_LINKS) ?? "";
174
188
  const dispatchContext = parseDispatchContextHeader(getAampHeader(headers, AAMP_HEADER.DISPATCH_CONTEXT));
175
189
  const sessionKey = getAampHeader(headers, AAMP_HEADER.SESSION_KEY);
176
190
  const parentTaskId = getAampHeader(headers, AAMP_HEADER.PARENT_TASK_ID);
@@ -184,7 +198,6 @@ function parseAampHeaders(meta) {
184
198
  title: decodedSubject.replace(/^\[AAMP Task\]\s*/, "").trim() || "Untitled Task",
185
199
  priority: priority === "urgent" || priority === "high" ? priority : "normal",
186
200
  ...expiresAt ? { expiresAt } : {},
187
- contextLinks: contextLinksStr ? contextLinksStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
188
201
  ...dispatchContext ? { dispatchContext } : {},
189
202
  ...parentTaskId ? { parentTaskId } : {},
190
203
  from,
@@ -273,6 +286,43 @@ function parseAampHeaders(meta) {
273
286
  };
274
287
  return streamOpened;
275
288
  }
289
+ if (intent === "pair.request") {
290
+ const pairCode = getAampHeader(headers, AAMP_HEADER.PAIR_CODE) ?? "";
291
+ if (!pairCode)
292
+ return null;
293
+ const pairRequest = {
294
+ protocolVersion,
295
+ intent: "pair.request",
296
+ taskId,
297
+ pairCode,
298
+ dispatchContextRules: decodeBase64UrlJson(getAampHeader(headers, AAMP_HEADER.DISPATCH_CONTEXT_RULES)) ?? {},
299
+ from,
300
+ to,
301
+ messageId: meta.messageId,
302
+ subject: meta.subject,
303
+ bodyText: ""
304
+ };
305
+ return pairRequest;
306
+ }
307
+ if (intent === "pair.respond") {
308
+ const rawStatus = getAampHeader(headers, AAMP_HEADER.STATUS);
309
+ const status = rawStatus === "completed" ? "completed" : "rejected";
310
+ const reason = getAampHeader(headers, AAMP_HEADER.ERROR_MSG) || void 0;
311
+ const pairRespond = {
312
+ protocolVersion,
313
+ intent: "pair.respond",
314
+ taskId,
315
+ status,
316
+ success: status === "completed",
317
+ reason,
318
+ from,
319
+ to,
320
+ messageId: meta.messageId,
321
+ subject: meta.subject,
322
+ bodyText: normalizeBodyText(meta.bodyText)
323
+ };
324
+ return pairRespond;
325
+ }
276
326
  if (intent === "card.query") {
277
327
  const cardQuery = {
278
328
  protocolVersion,
@@ -316,9 +366,6 @@ function buildDispatchHeaders(params) {
316
366
  if (params.sessionKey?.trim()) {
317
367
  headers[AAMP_HEADER.SESSION_KEY] = params.sessionKey.trim();
318
368
  }
319
- if (params.contextLinks.length > 0) {
320
- headers[AAMP_HEADER.CONTEXT_LINKS] = params.contextLinks.join(",");
321
- }
322
369
  const dispatchContext = serializeDispatchContextHeader(params.dispatchContext);
323
370
  if (dispatchContext) {
324
371
  headers[AAMP_HEADER.DISPATCH_CONTEXT] = dispatchContext;
@@ -350,6 +397,27 @@ function buildStreamOpenedHeaders(opts) {
350
397
  [AAMP_HEADER.STREAM_ID]: opts.streamId
351
398
  };
352
399
  }
400
+ function buildPairRequestHeaders(opts) {
401
+ return {
402
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
403
+ [AAMP_HEADER.INTENT]: "pair.request",
404
+ [AAMP_HEADER.TASK_ID]: opts.taskId,
405
+ [AAMP_HEADER.PAIR_CODE]: opts.pairCode,
406
+ [AAMP_HEADER.DISPATCH_CONTEXT_RULES]: encodeBase64UrlJson(opts.dispatchContextRules ?? {})
407
+ };
408
+ }
409
+ function buildPairRespondHeaders(opts) {
410
+ const headers = {
411
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
412
+ [AAMP_HEADER.INTENT]: "pair.respond",
413
+ [AAMP_HEADER.TASK_ID]: opts.taskId,
414
+ [AAMP_HEADER.STATUS]: opts.success ? "completed" : "rejected"
415
+ };
416
+ if (!opts.success && opts.reason?.trim()) {
417
+ headers[AAMP_HEADER.ERROR_MSG] = opts.reason.replace(/[\r\n]/g, " ").trim();
418
+ }
419
+ return headers;
420
+ }
353
421
  function buildResultHeaders(params) {
354
422
  const headers = {
355
423
  [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
@@ -735,6 +803,12 @@ var JmapPushClient = class extends TinyEmitter {
735
803
  case "task.stream.opened":
736
804
  this.emit("task.stream.opened", aampMsg);
737
805
  break;
806
+ case "pair.request":
807
+ this.emit("pair.request", aampMsg);
808
+ break;
809
+ case "pair.respond":
810
+ this.emit("pair.respond", aampMsg);
811
+ break;
738
812
  case "card.query":
739
813
  this.emit("card.query", aampMsg);
740
814
  break;
@@ -1120,10 +1194,177 @@ var JmapPushClient = class extends TinyEmitter {
1120
1194
  }
1121
1195
  };
1122
1196
 
1197
+ // ../sdk/dist/pairing.js
1198
+ import { randomBytes } from "node:crypto";
1199
+ function normalizeMailbox(value) {
1200
+ const mailbox = value.trim().toLowerCase();
1201
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(mailbox)) {
1202
+ throw new Error(`Invalid AAMP mailbox in pairing URL: ${value}`);
1203
+ }
1204
+ return mailbox;
1205
+ }
1206
+ function normalizeDispatchContextRules(rules) {
1207
+ if (!rules)
1208
+ return void 0;
1209
+ const normalized = Object.fromEntries(Object.entries(rules).map(([key, values]) => [
1210
+ key.trim().toLowerCase(),
1211
+ (Array.isArray(values) ? values : []).map((value) => value.trim()).filter(Boolean)
1212
+ ]).filter(([key, values]) => Boolean(key) && values.length > 0));
1213
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
1214
+ }
1215
+ function encodeBase64UrlJson2(value) {
1216
+ return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
1217
+ }
1218
+ function decodeDispatchContextRules(value) {
1219
+ const candidates = [
1220
+ () => Buffer.from(value, "base64url").toString("utf8"),
1221
+ () => value
1222
+ ];
1223
+ for (const read of candidates) {
1224
+ try {
1225
+ const parsed = JSON.parse(read());
1226
+ return normalizeDispatchContextRules(parsed);
1227
+ } catch {
1228
+ }
1229
+ }
1230
+ throw new Error("Invalid dispatch_context_rules in pairing URL");
1231
+ }
1232
+ function buildPairingUrl(payload) {
1233
+ const mailbox = normalizeMailbox(payload.mailbox);
1234
+ const pairCode = payload.pairCode.trim();
1235
+ if (!pairCode)
1236
+ throw new Error("pairCode cannot be empty");
1237
+ const url = new URL("aamp://connect");
1238
+ url.searchParams.set("mailbox", mailbox);
1239
+ url.searchParams.set("pair_code", pairCode);
1240
+ const rules = normalizeDispatchContextRules(payload.dispatchContextRules);
1241
+ if (rules) {
1242
+ url.searchParams.set("dispatch_context_rules", encodeBase64UrlJson2(rules));
1243
+ }
1244
+ return url.toString();
1245
+ }
1246
+ function createPairingCode(options) {
1247
+ const pairCode = options.pairCode?.trim() || randomBytes(6).toString("base64url");
1248
+ const dispatchContextRules = normalizeDispatchContextRules(options.dispatchContextRules);
1249
+ return {
1250
+ mailbox: normalizeMailbox(options.mailbox),
1251
+ pairCode,
1252
+ expiresAt: new Date(Date.now() + (options.ttlSeconds ?? 300) * 1e3).toISOString(),
1253
+ connectUrl: buildPairingUrl({
1254
+ mailbox: options.mailbox,
1255
+ pairCode,
1256
+ ...dispatchContextRules ? { dispatchContextRules } : {}
1257
+ }),
1258
+ ...dispatchContextRules ? { dispatchContextRules } : {}
1259
+ };
1260
+ }
1261
+ function parsePairingUrl(input) {
1262
+ let url;
1263
+ try {
1264
+ url = new URL(input.trim());
1265
+ } catch {
1266
+ throw new Error("Invalid pairing URL");
1267
+ }
1268
+ if (url.protocol !== "aamp:" || url.hostname !== "connect") {
1269
+ throw new Error("Pairing URL must start with aamp://connect");
1270
+ }
1271
+ const mailbox = url.searchParams.get("mailbox") ?? "";
1272
+ const pairCode = url.searchParams.get("pair_code") ?? "";
1273
+ if (!pairCode.trim())
1274
+ throw new Error("Pairing URL is missing pair_code");
1275
+ const rawRules = url.searchParams.get("dispatch_context_rules") ?? url.searchParams.get("dispatchContextRules");
1276
+ const dispatchContextRules = rawRules ? decodeDispatchContextRules(rawRules) : void 0;
1277
+ return {
1278
+ mailbox: normalizeMailbox(mailbox),
1279
+ pairCode: pairCode.trim(),
1280
+ ...dispatchContextRules ? { dispatchContextRules } : {}
1281
+ };
1282
+ }
1283
+ function isPairingUrl(input) {
1284
+ try {
1285
+ parsePairingUrl(input);
1286
+ return true;
1287
+ } catch {
1288
+ return false;
1289
+ }
1290
+ }
1291
+ function consumePairingCode(state, options) {
1292
+ if (state.consumedAt)
1293
+ return null;
1294
+ if (normalizeMailbox(state.mailbox) !== normalizeMailbox(options.mailbox))
1295
+ return null;
1296
+ if (state.pairCode !== options.pairCode.trim())
1297
+ return null;
1298
+ if (new Date(state.expiresAt).getTime() <= (options.now ?? /* @__PURE__ */ new Date()).getTime())
1299
+ return null;
1300
+ return {
1301
+ ...state,
1302
+ pairCode: "",
1303
+ connectUrl: "",
1304
+ consumedAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString()
1305
+ };
1306
+ }
1307
+ function createPairedSenderPolicy(request, pairedAt = /* @__PURE__ */ new Date()) {
1308
+ return {
1309
+ sender: normalizeMailbox(request.from),
1310
+ dispatchContextRules: normalizeDispatchContextRules(request.dispatchContextRules) ?? {},
1311
+ pairedAt: pairedAt.toISOString()
1312
+ };
1313
+ }
1314
+ function upsertPairedSenderPolicy(policies, policy) {
1315
+ const sender = normalizeMailbox(policy.sender);
1316
+ return [
1317
+ ...policies.filter((item) => normalizeMailbox(item.sender) !== sender),
1318
+ {
1319
+ ...policy,
1320
+ sender,
1321
+ dispatchContextRules: normalizeDispatchContextRules(policy.dispatchContextRules) ?? {}
1322
+ }
1323
+ ];
1324
+ }
1325
+ function matchPairedSenderPolicy(policies, sender, dispatchContext) {
1326
+ if (policies.length === 0) {
1327
+ return { allowed: false, reason: "no paired sender policies configured" };
1328
+ }
1329
+ const normalizedSender = normalizeMailbox(sender);
1330
+ const policy = policies.find((item) => normalizeMailbox(item.sender) === normalizedSender);
1331
+ if (!policy) {
1332
+ return { allowed: false, reason: `sender ${sender} is not paired` };
1333
+ }
1334
+ for (const [key, allowedValues] of Object.entries(policy.dispatchContextRules ?? {})) {
1335
+ if (!Array.isArray(allowedValues) || allowedValues.length === 0)
1336
+ continue;
1337
+ const observed = dispatchContext?.[key];
1338
+ if (!observed || !allowedValues.includes(observed)) {
1339
+ return { allowed: false, reason: `dispatchContext does not match paired sender policy for ${sender}` };
1340
+ }
1341
+ }
1342
+ return { allowed: true };
1343
+ }
1344
+
1123
1345
  // ../sdk/dist/smtp-sender.js
1124
1346
  import { createTransport } from "nodemailer";
1125
1347
  import { randomUUID } from "crypto";
1126
1348
  var sanitize = (s) => s.replace(/[\r\n]/g, " ").trim();
1349
+ var HTTP_SEND_MAX_ATTEMPTS = 4;
1350
+ var HTTP_SEND_RETRY_BASE_DELAY_MS = 500;
1351
+ function sleep2(ms) {
1352
+ return new Promise((resolve) => setTimeout(resolve, ms));
1353
+ }
1354
+ function isRetryableHttpStatus(status) {
1355
+ return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
1356
+ }
1357
+ function describeError2(err) {
1358
+ if (!(err instanceof Error))
1359
+ return String(err);
1360
+ const details = err;
1361
+ const parts = [err.message];
1362
+ if (details.code)
1363
+ parts.push(`code=${details.code}`);
1364
+ if (details.cause instanceof Error)
1365
+ parts.push(`cause=${describeError2(details.cause)}`);
1366
+ return parts.join(" | ");
1367
+ }
1127
1368
  function deriveMailboxServiceDefaults(email, baseUrl2) {
1128
1369
  const domain = email.split("@")[1]?.trim();
1129
1370
  const resolvedBaseUrl = baseUrl2?.trim() || (domain ? `https://${domain}` : void 0);
@@ -1136,6 +1377,7 @@ function deriveMailboxServiceDefaults(email, baseUrl2) {
1136
1377
  var SmtpSender = class _SmtpSender {
1137
1378
  config;
1138
1379
  transport;
1380
+ fetch;
1139
1381
  discoveredApiUrlPromise = null;
1140
1382
  jmapSessionPromise = null;
1141
1383
  sentMailboxIdPromise = null;
@@ -1148,12 +1390,16 @@ var SmtpSender = class _SmtpSender {
1148
1390
  password: config.password,
1149
1391
  httpBaseUrl: derived.httpBaseUrl,
1150
1392
  authToken: Buffer.from(`${config.email}:${config.password}`).toString("base64"),
1393
+ fetch: config.fetch,
1394
+ forceHttpSend: config.forceHttpSend,
1395
+ persistSentCopy: config.persistSentCopy,
1151
1396
  secure: config.secure,
1152
1397
  rejectUnauthorized: config.rejectUnauthorized
1153
1398
  });
1154
1399
  }
1155
1400
  constructor(config) {
1156
1401
  this.config = config;
1402
+ this.fetch = config.fetch ?? fetch;
1157
1403
  this.transport = createTransport({
1158
1404
  host: config.host,
1159
1405
  port: config.port,
@@ -1174,6 +1420,9 @@ var SmtpSender = class _SmtpSender {
1174
1420
  return email.split("@")[1]?.toLowerCase() ?? "";
1175
1421
  }
1176
1422
  shouldUseHttpFallback(to) {
1423
+ if (this.config.forceHttpSend) {
1424
+ return Boolean(this.config.httpBaseUrl && this.config.authToken);
1425
+ }
1177
1426
  return Boolean(this.config.httpBaseUrl && this.config.authToken && this.senderDomain() && this.senderDomain() === this.recipientDomain(to));
1178
1427
  }
1179
1428
  async resolveAampApiUrl() {
@@ -1183,7 +1432,7 @@ var SmtpSender = class _SmtpSender {
1183
1432
  }
1184
1433
  if (!this.discoveredApiUrlPromise) {
1185
1434
  this.discoveredApiUrlPromise = (async () => {
1186
- const discoveryRes = await fetch(`${base}/.well-known/aamp`);
1435
+ const discoveryRes = await this.fetch(`${base}/.well-known/aamp`);
1187
1436
  if (!discoveryRes.ok) {
1188
1437
  throw new Error(`AAMP discovery failed: ${discoveryRes.status}`);
1189
1438
  }
@@ -1205,33 +1454,49 @@ var SmtpSender = class _SmtpSender {
1205
1454
  if (!this.config.authToken) {
1206
1455
  throw new Error("HTTP send fallback is not configured");
1207
1456
  }
1208
- const apiUrl = new URL(await this.resolveAampApiUrl());
1209
- apiUrl.searchParams.set("action", "aamp.mailbox.send");
1210
- const res = await fetch(apiUrl, {
1211
- method: "POST",
1212
- headers: {
1213
- Authorization: `Basic ${this.config.authToken}`,
1214
- "Content-Type": "application/json"
1215
- },
1216
- body: JSON.stringify({
1217
- to: opts.to,
1218
- subject: opts.subject,
1219
- text: opts.text,
1220
- aampHeaders: opts.aampHeaders,
1221
- attachments: opts.attachments?.map((a) => ({
1222
- filename: a.filename,
1223
- contentType: a.contentType,
1224
- content: typeof a.content === "string" ? a.content : a.content.toString("base64")
1225
- }))
1226
- })
1227
- });
1228
- const data = await res.json().catch(() => ({}));
1229
- if (!res.ok) {
1230
- throw new Error(data.details || `HTTP send failed: ${res.status}`);
1457
+ let lastError = null;
1458
+ for (let attempt = 1; attempt <= HTTP_SEND_MAX_ATTEMPTS; attempt += 1) {
1459
+ const apiUrl = new URL(await this.resolveAampApiUrl());
1460
+ apiUrl.searchParams.set("action", "aamp.mailbox.send");
1461
+ try {
1462
+ const res = await this.fetch(apiUrl, {
1463
+ method: "POST",
1464
+ headers: {
1465
+ Authorization: `Basic ${this.config.authToken}`,
1466
+ "Content-Type": "application/json"
1467
+ },
1468
+ body: JSON.stringify({
1469
+ to: opts.to,
1470
+ subject: opts.subject,
1471
+ text: opts.text,
1472
+ aampHeaders: opts.aampHeaders,
1473
+ attachments: opts.attachments?.map((a) => ({
1474
+ filename: a.filename,
1475
+ contentType: a.contentType,
1476
+ content: typeof a.content === "string" ? a.content : a.content.toString("base64")
1477
+ }))
1478
+ })
1479
+ });
1480
+ const data = await res.json().catch(() => ({}));
1481
+ if (res.ok)
1482
+ return { messageId: data.messageId };
1483
+ lastError = new Error(data.details || `HTTP send failed: ${res.status}`);
1484
+ if (!isRetryableHttpStatus(res.status) || attempt === HTTP_SEND_MAX_ATTEMPTS)
1485
+ break;
1486
+ } catch (err) {
1487
+ lastError = err instanceof Error ? err : new Error(String(err));
1488
+ if (attempt === HTTP_SEND_MAX_ATTEMPTS) {
1489
+ lastError = new Error(`HTTP send failed after ${attempt} attempts: ${describeError2(lastError)}`);
1490
+ break;
1491
+ }
1492
+ }
1493
+ await sleep2(HTTP_SEND_RETRY_BASE_DELAY_MS * attempt);
1231
1494
  }
1232
- return { messageId: data.messageId };
1495
+ throw lastError ?? new Error("HTTP send failed");
1233
1496
  }
1234
1497
  canPersistSentCopy() {
1498
+ if (this.config.persistSentCopy === false)
1499
+ return false;
1235
1500
  return Boolean(this.config.httpBaseUrl && this.config.authToken);
1236
1501
  }
1237
1502
  getJmapAuthHeader() {
@@ -1247,7 +1512,7 @@ var SmtpSender = class _SmtpSender {
1247
1512
  }
1248
1513
  if (!this.jmapSessionPromise) {
1249
1514
  this.jmapSessionPromise = (async () => {
1250
- const res = await fetch(`${base}/.well-known/jmap`, {
1515
+ const res = await this.fetch(`${base}/.well-known/jmap`, {
1251
1516
  headers: { Authorization: this.getJmapAuthHeader() }
1252
1517
  });
1253
1518
  if (!res.ok) {
@@ -1273,7 +1538,7 @@ var SmtpSender = class _SmtpSender {
1273
1538
  }
1274
1539
  async jmapCall(methodCalls) {
1275
1540
  const session = await this.resolveJmapSession();
1276
- const res = await fetch(session.apiUrl, {
1541
+ const res = await this.fetch(session.apiUrl, {
1277
1542
  method: "POST",
1278
1543
  headers: {
1279
1544
  Authorization: this.getJmapAuthHeader(),
@@ -1370,7 +1635,6 @@ var SmtpSender = class _SmtpSender {
1370
1635
  taskId,
1371
1636
  priority: opts.priority,
1372
1637
  expiresAt: opts.expiresAt,
1373
- contextLinks: opts.contextLinks ?? [],
1374
1638
  dispatchContext: opts.dispatchContext,
1375
1639
  parentTaskId: opts.parentTaskId
1376
1640
  });
@@ -1383,8 +1647,6 @@ var SmtpSender = class _SmtpSender {
1383
1647
  `Task ID: ${taskId}`,
1384
1648
  `Priority: ${opts.priority ?? "normal"}`,
1385
1649
  opts.expiresAt ? `Expires At: ${opts.expiresAt}` : `Expires At: none`,
1386
- opts.contextLinks?.length ? `Context:
1387
- ${opts.contextLinks.map((l) => ` ${l}`).join("\n")}` : "",
1388
1650
  opts.bodyText ?? "",
1389
1651
  ``,
1390
1652
  `--- This email was sent by AAMP. Reply directly to submit your result. ---`
@@ -1727,6 +1989,110 @@ Stream ID: ${opts.streamId}`,
1727
1989
  references: opts.inReplyTo
1728
1990
  });
1729
1991
  }
1992
+ async sendPairRequest(opts) {
1993
+ const taskId = opts.taskId ?? randomUUID();
1994
+ const aampHeaders = buildPairRequestHeaders({
1995
+ taskId,
1996
+ pairCode: opts.pairCode,
1997
+ dispatchContextRules: opts.dispatchContextRules ?? {}
1998
+ });
1999
+ const text = [
2000
+ "AAMP Pair Request",
2001
+ "",
2002
+ `Pair code: ${opts.pairCode}`,
2003
+ `Dispatch context rules: ${JSON.stringify(opts.dispatchContextRules ?? {})}`
2004
+ ].join("\n");
2005
+ const mailOpts = {
2006
+ from: this.config.user,
2007
+ to: opts.to,
2008
+ subject: "[AAMP Pair] Connection request",
2009
+ text,
2010
+ headers: aampHeaders
2011
+ };
2012
+ if (this.shouldUseHttpFallback(opts.to)) {
2013
+ const info2 = await this.sendViaHttp({
2014
+ to: opts.to,
2015
+ subject: mailOpts.subject,
2016
+ text,
2017
+ aampHeaders
2018
+ });
2019
+ await this.saveToSentBestEffort({
2020
+ from: this.config.user,
2021
+ to: opts.to,
2022
+ subject: mailOpts.subject,
2023
+ text,
2024
+ aampHeaders,
2025
+ messageId: info2.messageId
2026
+ });
2027
+ return { taskId, messageId: info2.messageId ?? "" };
2028
+ }
2029
+ const info = await this.transport.sendMail(mailOpts);
2030
+ await this.saveToSentBestEffort({
2031
+ from: this.config.user,
2032
+ to: opts.to,
2033
+ subject: mailOpts.subject,
2034
+ text,
2035
+ aampHeaders,
2036
+ messageId: info.messageId
2037
+ });
2038
+ return { taskId, messageId: info.messageId ?? "" };
2039
+ }
2040
+ async sendPairRespond(opts) {
2041
+ const aampHeaders = buildPairRespondHeaders({
2042
+ taskId: opts.taskId,
2043
+ success: opts.success,
2044
+ reason: opts.reason
2045
+ });
2046
+ const status = opts.success ? "completed" : "rejected";
2047
+ const text = [
2048
+ "AAMP Pair Response",
2049
+ "",
2050
+ `Task ID: ${opts.taskId}`,
2051
+ `Status: ${status}`,
2052
+ ...opts.reason?.trim() ? ["", `Reason: ${opts.reason.trim()}`] : []
2053
+ ].join("\n");
2054
+ const mailOpts = {
2055
+ from: this.config.user,
2056
+ to: opts.to,
2057
+ subject: `[AAMP Pair] ${status}`,
2058
+ text,
2059
+ headers: aampHeaders
2060
+ };
2061
+ if (opts.inReplyTo) {
2062
+ mailOpts.inReplyTo = opts.inReplyTo;
2063
+ mailOpts.references = opts.inReplyTo;
2064
+ }
2065
+ if (this.shouldUseHttpFallback(opts.to)) {
2066
+ const info2 = await this.sendViaHttp({
2067
+ to: opts.to,
2068
+ subject: mailOpts.subject,
2069
+ text,
2070
+ aampHeaders
2071
+ });
2072
+ await this.saveToSentBestEffort({
2073
+ from: this.config.user,
2074
+ to: opts.to,
2075
+ subject: mailOpts.subject,
2076
+ text,
2077
+ aampHeaders,
2078
+ messageId: info2.messageId,
2079
+ inReplyTo: opts.inReplyTo,
2080
+ references: opts.inReplyTo
2081
+ });
2082
+ return;
2083
+ }
2084
+ const info = await this.transport.sendMail(mailOpts);
2085
+ await this.saveToSentBestEffort({
2086
+ from: this.config.user,
2087
+ to: opts.to,
2088
+ subject: mailOpts.subject,
2089
+ text,
2090
+ aampHeaders,
2091
+ messageId: info.messageId,
2092
+ inReplyTo: opts.inReplyTo,
2093
+ references: opts.inReplyTo
2094
+ });
2095
+ }
1730
2096
  async sendCardQuery(opts) {
1731
2097
  const taskId = opts.taskId ?? randomUUID();
1732
2098
  const aampHeaders = buildCardQueryHeaders({ taskId });
@@ -1967,6 +2333,9 @@ var AampClient = class _AampClient extends TinyEmitter {
1967
2333
  password: config.smtpPassword,
1968
2334
  httpBaseUrl: config.httpSendBaseUrl ?? resolvedBaseUrl,
1969
2335
  authToken: mailboxToken,
2336
+ fetch: config.fetch,
2337
+ forceHttpSend: config.forceHttpSend,
2338
+ persistSentCopy: config.persistSentCopy,
1970
2339
  rejectUnauthorized: config.rejectUnauthorized
1971
2340
  });
1972
2341
  this.jmapClient.on("task.dispatch", (task) => {
@@ -1987,6 +2356,12 @@ var AampClient = class _AampClient extends TinyEmitter {
1987
2356
  this.jmapClient.on("task.stream.opened", (stream) => {
1988
2357
  this.emit("task.stream.opened", stream);
1989
2358
  });
2359
+ this.jmapClient.on("pair.request", (request) => {
2360
+ this.emit("pair.request", request);
2361
+ });
2362
+ this.jmapClient.on("pair.respond", (response) => {
2363
+ this.emit("pair.respond", response);
2364
+ });
1990
2365
  this.jmapClient.on("card.query", (query) => {
1991
2366
  this.emit("card.query", query);
1992
2367
  });
@@ -2024,12 +2399,23 @@ var AampClient = class _AampClient extends TinyEmitter {
2024
2399
  smtpPassword: config.smtpPassword,
2025
2400
  reconnectInterval: config.reconnectInterval,
2026
2401
  taskDispatchConcurrency: config.taskDispatchConcurrency,
2402
+ fetch: config.fetch,
2403
+ forceHttpSend: config.forceHttpSend,
2404
+ persistSentCopy: config.persistSentCopy,
2027
2405
  rejectUnauthorized: config.rejectUnauthorized
2028
2406
  });
2029
2407
  }
2030
- static async discoverAampService(aampHost) {
2408
+ static createPairingCode = createPairingCode;
2409
+ static buildPairingUrl = buildPairingUrl;
2410
+ static parsePairingUrl = parsePairingUrl;
2411
+ static isPairingUrl = isPairingUrl;
2412
+ static consumePairingCode = consumePairingCode;
2413
+ static createPairedSenderPolicy = createPairedSenderPolicy;
2414
+ static upsertPairedSenderPolicy = upsertPairedSenderPolicy;
2415
+ static matchPairedSenderPolicy = matchPairedSenderPolicy;
2416
+ static async discoverAampService(aampHost, fetchImpl = fetch) {
2031
2417
  const base = aampHost.replace(/\/$/, "");
2032
- const res = await fetch(`${base}/.well-known/aamp`);
2418
+ const res = await fetchImpl(`${base}/.well-known/aamp`);
2033
2419
  if (!res.ok) {
2034
2420
  throw new Error(`AAMP discovery failed: ${res.status} ${res.statusText}`);
2035
2421
  }
@@ -2040,7 +2426,8 @@ var AampClient = class _AampClient extends TinyEmitter {
2040
2426
  return discovery;
2041
2427
  }
2042
2428
  static async callDiscoveredApi(base, opts) {
2043
- const discovery = await _AampClient.discoverAampService(base);
2429
+ const fetchImpl = opts.fetch ?? fetch;
2430
+ const discovery = await _AampClient.discoverAampService(base, fetchImpl);
2044
2431
  const apiUrl = new URL(discovery.api.url, `${base}/`);
2045
2432
  apiUrl.searchParams.set("action", opts.action);
2046
2433
  for (const [key, value] of Object.entries(opts.query ?? {})) {
@@ -2048,7 +2435,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2048
2435
  continue;
2049
2436
  apiUrl.searchParams.set(key, String(value));
2050
2437
  }
2051
- return fetch(apiUrl, {
2438
+ return fetchImpl(apiUrl, {
2052
2439
  method: opts.method ?? "GET",
2053
2440
  headers: {
2054
2441
  ...opts.authToken ? { Authorization: `Basic ${opts.authToken}` } : {},
@@ -2157,7 +2544,6 @@ var AampClient = class _AampClient extends TinyEmitter {
2157
2544
  priority: opts.priority,
2158
2545
  expiresAt: opts.expiresAt,
2159
2546
  sessionKey: opts.sessionKey,
2160
- contextLinks: opts.contextLinks,
2161
2547
  dispatchContext: opts.dispatchContext,
2162
2548
  parentTaskId: opts.parentTaskId,
2163
2549
  attachments: opts.attachments
@@ -2181,6 +2567,12 @@ var AampClient = class _AampClient extends TinyEmitter {
2181
2567
  async sendStreamOpened(opts) {
2182
2568
  return this.smtpSender.sendStreamOpened(opts);
2183
2569
  }
2570
+ async sendPairRequest(opts) {
2571
+ return this.smtpSender.sendPairRequest(opts);
2572
+ }
2573
+ async sendPairRespond(opts) {
2574
+ return this.smtpSender.sendPairRespond(opts);
2575
+ }
2184
2576
  async sendCardQuery(opts) {
2185
2577
  return this.smtpSender.sendCardQuery(opts);
2186
2578
  }
@@ -2194,6 +2586,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2194
2586
  action: "aamp.directory.upsert",
2195
2587
  method: "POST",
2196
2588
  authToken: mailboxToken,
2589
+ fetch: this.config.fetch,
2197
2590
  body: opts
2198
2591
  });
2199
2592
  if (!res.ok) {
@@ -2209,6 +2602,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2209
2602
  const res = await _AampClient.callDiscoveredApi(base, {
2210
2603
  action: "aamp.directory.list",
2211
2604
  authToken: mailboxToken,
2605
+ fetch: this.config.fetch,
2212
2606
  query: {
2213
2607
  scope: opts.scope,
2214
2608
  includeSelf: opts.includeSelf,
@@ -2228,6 +2622,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2228
2622
  const res = await _AampClient.callDiscoveredApi(base, {
2229
2623
  action: "aamp.directory.search",
2230
2624
  authToken: mailboxToken,
2625
+ fetch: this.config.fetch,
2231
2626
  query: {
2232
2627
  q: opts.query,
2233
2628
  scope: opts.scope,
@@ -2248,6 +2643,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2248
2643
  const res = await _AampClient.callDiscoveredApi(base, {
2249
2644
  action: "aamp.mailbox.thread",
2250
2645
  authToken: mailboxToken,
2646
+ fetch: this.config.fetch,
2251
2647
  query: {
2252
2648
  taskId,
2253
2649
  includeStreamOpened: opts.includeStreamOpened
@@ -2273,7 +2669,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2273
2669
  };
2274
2670
  }
2275
2671
  async resolveStreamCapability() {
2276
- const discovery = await _AampClient.discoverAampService(this.config.baseUrl);
2672
+ const discovery = await _AampClient.discoverAampService(this.config.baseUrl, this.config.fetch);
2277
2673
  const stream = discovery.capabilities?.stream;
2278
2674
  if (!stream?.transport) {
2279
2675
  throw new Error("AAMP stream capability is not available on this service");
@@ -2286,6 +2682,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2286
2682
  action: stream.createAction ?? "aamp.stream.create",
2287
2683
  method: "POST",
2288
2684
  authToken: this.config.mailboxToken,
2685
+ fetch: this.config.fetch,
2289
2686
  body: opts
2290
2687
  });
2291
2688
  if (!res.ok) {
@@ -2332,6 +2729,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2332
2729
  action: stream.appendAction ?? "aamp.stream.append",
2333
2730
  method: "POST",
2334
2731
  authToken: this.config.mailboxToken,
2732
+ fetch: this.config.fetch,
2335
2733
  body: opts
2336
2734
  });
2337
2735
  if (!res.ok) {
@@ -2437,6 +2835,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2437
2835
  action: stream.closeAction ?? "aamp.stream.close",
2438
2836
  method: "POST",
2439
2837
  authToken: this.config.mailboxToken,
2838
+ fetch: this.config.fetch,
2440
2839
  body: opts
2441
2840
  });
2442
2841
  if (!res.ok) {
@@ -2450,6 +2849,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2450
2849
  const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
2451
2850
  action: stream.getAction ?? "aamp.stream.get",
2452
2851
  authToken: this.config.mailboxToken,
2852
+ fetch: this.config.fetch,
2453
2853
  query: {
2454
2854
  ...opts.taskId ? { taskId: opts.taskId } : {},
2455
2855
  ...opts.streamId ? { streamId: opts.streamId } : {}
@@ -2476,7 +2876,8 @@ var AampClient = class _AampClient extends TinyEmitter {
2476
2876
  if (opts.signal) {
2477
2877
  opts.signal.addEventListener("abort", () => controller.abort(), { once: true });
2478
2878
  }
2479
- const res = await fetch(url, {
2879
+ const fetchImpl = this.config.fetch ?? fetch;
2880
+ const res = await fetchImpl(url, {
2480
2881
  headers: {
2481
2882
  Authorization: `Basic ${this.config.mailboxToken}`,
2482
2883
  Accept: "text/event-stream"
@@ -2587,12 +2988,19 @@ import { readFileSync as readFileSync2 } from "node:fs";
2587
2988
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2588
2989
  import { dirname, join } from "node:path";
2589
2990
  import { homedir } from "node:os";
2991
+ import { randomBytes as randomBytes2 } from "node:crypto";
2590
2992
  function defaultCredentialsPath() {
2591
2993
  return join(homedir(), ".openclaw", "extensions", "aamp-openclaw-plugin", ".credentials.json");
2592
2994
  }
2593
2995
  function defaultTaskStatePath() {
2594
2996
  return join(homedir(), ".openclaw", "extensions", "aamp-openclaw-plugin", ".task-state.json");
2595
2997
  }
2998
+ function defaultPairingPath() {
2999
+ return join(homedir(), ".openclaw", "extensions", "aamp-openclaw-plugin", ".pairing.json");
3000
+ }
3001
+ function defaultSenderPoliciesPath() {
3002
+ return join(homedir(), ".openclaw", "extensions", "aamp-openclaw-plugin", ".sender-policies.json");
3003
+ }
2596
3004
  function loadCachedIdentity(file) {
2597
3005
  const resolved = file ?? defaultCredentialsPath();
2598
3006
  if (!existsSync(resolved))
@@ -2639,6 +3047,66 @@ function saveTaskState(state, file) {
2639
3047
  terminalTaskIds: state.terminalTaskIds ?? []
2640
3048
  }, null, 2), "utf-8");
2641
3049
  }
3050
+ function createPairingCode2(params) {
3051
+ const pairCode = randomBytes2(6).toString("base64url");
3052
+ const expiresAt = new Date(Date.now() + (params.ttlSeconds ?? 300) * 1e3).toISOString();
3053
+ const connectUrl = `aamp://connect?mailbox=${encodeURIComponent(params.mailbox.toLowerCase())}&pair_code=${encodeURIComponent(pairCode)}`;
3054
+ const state = {
3055
+ mailbox: params.mailbox.toLowerCase(),
3056
+ pairCode,
3057
+ expiresAt,
3058
+ connectUrl
3059
+ };
3060
+ const resolved = params.file ?? defaultPairingPath();
3061
+ mkdirSync(dirname(resolved), { recursive: true });
3062
+ writeFileSync(resolved, JSON.stringify(state, null, 2), "utf-8");
3063
+ return state;
3064
+ }
3065
+ function consumePairingCode2(params) {
3066
+ const resolved = params.file ?? defaultPairingPath();
3067
+ if (!existsSync(resolved))
3068
+ return null;
3069
+ const state = JSON.parse(readFileSync(resolved, "utf-8"));
3070
+ if (state.mailbox.toLowerCase() !== params.mailbox.toLowerCase())
3071
+ return null;
3072
+ if (state.pairCode !== params.pairCode)
3073
+ return null;
3074
+ if (state.consumedAt)
3075
+ return null;
3076
+ if (new Date(state.expiresAt).getTime() <= Date.now())
3077
+ return null;
3078
+ writeFileSync(resolved, JSON.stringify({ ...state, pairCode: "", consumedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2), "utf-8");
3079
+ return state;
3080
+ }
3081
+ function isPairedSenderPolicy(value) {
3082
+ if (!value || typeof value !== "object")
3083
+ return false;
3084
+ const policy = value;
3085
+ return typeof policy.sender === "string" && typeof policy.dispatchContextRules === "object" && typeof policy.pairedAt === "string";
3086
+ }
3087
+ function loadPairedSenderPolicies(file) {
3088
+ const resolved = file ?? defaultSenderPoliciesPath();
3089
+ if (!existsSync(resolved))
3090
+ return [];
3091
+ try {
3092
+ const data = JSON.parse(readFileSync(resolved, "utf-8"));
3093
+ return Array.isArray(data) ? data.filter(isPairedSenderPolicy) : [];
3094
+ } catch {
3095
+ return [];
3096
+ }
3097
+ }
3098
+ function addPairedSenderPolicy(file, policy) {
3099
+ const resolved = file ?? defaultSenderPoliciesPath();
3100
+ const policies = loadPairedSenderPolicies(resolved);
3101
+ const normalizedSender = policy.sender.toLowerCase();
3102
+ const next = [
3103
+ ...policies.filter((item) => item.sender.toLowerCase() !== normalizedSender),
3104
+ { ...policy, sender: normalizedSender }
3105
+ ];
3106
+ mkdirSync(dirname(resolved), { recursive: true });
3107
+ writeFileSync(resolved, JSON.stringify(next, null, 2), "utf-8");
3108
+ return next;
3109
+ }
2642
3110
  function ensureDir(dir) {
2643
3111
  mkdirSync(dir, { recursive: true });
2644
3112
  }
@@ -2653,7 +3121,7 @@ function writeBinaryFile(path, content) {
2653
3121
  // src/index.ts
2654
3122
  function matchSenderPolicy(task, senderPolicies) {
2655
3123
  if (!senderPolicies?.length)
2656
- return { allowed: true };
3124
+ return { allowed: false, reason: "no configured senderPolicies" };
2657
3125
  const sender = task.from.toLowerCase();
2658
3126
  const policy = senderPolicies.find((item) => item.sender.trim().toLowerCase() === sender);
2659
3127
  if (!policy) {
@@ -2682,12 +3150,62 @@ function matchSenderPolicy(task, senderPolicies) {
2682
3150
  }
2683
3151
  return { allowed: true };
2684
3152
  }
3153
+ function rulesMatch(rules, dispatchContext) {
3154
+ for (const [key, allowedValues] of Object.entries(rules ?? {})) {
3155
+ if (!Array.isArray(allowedValues) || allowedValues.length === 0)
3156
+ continue;
3157
+ const observed = dispatchContext?.[key];
3158
+ if (!observed || !allowedValues.includes(observed))
3159
+ return false;
3160
+ }
3161
+ return true;
3162
+ }
3163
+ function matchPairedSenderPolicy2(task, senderPolicies) {
3164
+ if (senderPolicies.length === 0)
3165
+ return { allowed: false, reason: "no paired sender policies configured" };
3166
+ const sender = task.from.toLowerCase();
3167
+ const policy = senderPolicies.find((item) => item.sender.trim().toLowerCase() === sender);
3168
+ if (!policy) {
3169
+ return { allowed: false, reason: `sender ${task.from} is not paired` };
3170
+ }
3171
+ if (!rulesMatch(policy.dispatchContextRules, task.dispatchContext)) {
3172
+ return { allowed: false, reason: `dispatchContext does not match paired sender policy for ${task.from}` };
3173
+ }
3174
+ return { allowed: true };
3175
+ }
3176
+ function matchCombinedSenderPolicy(task, configuredPolicies, pairedPolicies) {
3177
+ const hasConfiguredPolicies = (configuredPolicies?.length ?? 0) > 0;
3178
+ const hasPairedPolicies = pairedPolicies.length > 0;
3179
+ if (!hasConfiguredPolicies && !hasPairedPolicies) {
3180
+ return { allowed: false, reason: "no sender policy configured" };
3181
+ }
3182
+ const configuredDecision = hasConfiguredPolicies ? matchSenderPolicy(task, configuredPolicies) : { allowed: false, reason: "no configured senderPolicies" };
3183
+ const pairedDecision = hasPairedPolicies ? matchPairedSenderPolicy2(task, pairedPolicies) : { allowed: false, reason: "no paired sender policies configured" };
3184
+ if (pairedDecision.allowed)
3185
+ return pairedDecision;
3186
+ if (configuredDecision.allowed)
3187
+ return configuredDecision;
3188
+ return configuredDecision.reason ? configuredDecision : pairedDecision;
3189
+ }
2685
3190
  function baseUrl(aampHost) {
2686
3191
  if (aampHost.startsWith("http://") || aampHost.startsWith("https://")) {
2687
3192
  return aampHost.replace(/\/$/, "");
2688
3193
  }
2689
3194
  return `https://${aampHost}`;
2690
3195
  }
3196
+ async function renderTerminalQr(value) {
3197
+ try {
3198
+ const qrcode = await import("qrcode-terminal");
3199
+ const generator = qrcode.default?.generate ?? qrcode.generate;
3200
+ if (!generator)
3201
+ return "";
3202
+ return await new Promise((resolve) => {
3203
+ generator(value, { small: true }, (qr) => resolve(qr));
3204
+ });
3205
+ } catch {
3206
+ return "";
3207
+ }
3208
+ }
2691
3209
  var pendingTasks = /* @__PURE__ */ new Map();
2692
3210
  var activeTaskStreams = /* @__PURE__ */ new Map();
2693
3211
  var terminalTaskIds = new Set(loadTaskState(defaultTaskStatePath()).terminalTaskIds ?? []);
@@ -2711,6 +3229,7 @@ var transportMonitorTimer = null;
2711
3229
  var historicalReconcileCompleted = false;
2712
3230
  var channelRuntime = null;
2713
3231
  var channelCfg = null;
3232
+ var pairedSenderPolicies = [];
2714
3233
  async function ensureTaskStream(task) {
2715
3234
  if (!aampClient?.isConnected())
2716
3235
  return null;
@@ -2953,7 +3472,6 @@ function queuePendingTask(task) {
2953
3472
  threadContextText: task.threadContextText ?? "",
2954
3473
  priority: task.priority ?? "normal",
2955
3474
  ...task.expiresAt ? { expiresAt: task.expiresAt } : {},
2956
- contextLinks: task.contextLinks ?? [],
2957
3475
  messageId: task.messageId ?? "",
2958
3476
  receivedAt: (/* @__PURE__ */ new Date()).toISOString()
2959
3477
  });
@@ -3016,6 +3534,14 @@ var src_default = {
3016
3534
  type: "string",
3017
3535
  description: "Absolute path to cache AAMP credentials between gateway restarts. Default: ~/.openclaw/extensions/aamp-openclaw-plugin/.credentials.json. Delete this file to force re-registration with a new mailbox."
3018
3536
  },
3537
+ pairingFile: {
3538
+ type: "string",
3539
+ description: "Absolute path to store the current one-time AAMP pairing code. Default: ~/.openclaw/extensions/aamp-openclaw-plugin/.pairing.json."
3540
+ },
3541
+ senderPoliciesFile: {
3542
+ type: "string",
3543
+ description: "Absolute path to persist senders approved by pair.request. Default: ~/.openclaw/extensions/aamp-openclaw-plugin/.sender-policies.json."
3544
+ },
3019
3545
  senderPolicies: {
3020
3546
  type: "array",
3021
3547
  description: "Per-sender authorization policies. Each sender can optionally require specific X-AAMP-Dispatch-Context key/value pairs before a task is accepted.",
@@ -3106,6 +3632,70 @@ var src_default = {
3106
3632
  });
3107
3633
  api.logger.info(`[AAMP] Directory profile synced${cardText ? " (card text registered)" : ""}`);
3108
3634
  }
3635
+ async function sendPairResponse(request, success, reason) {
3636
+ if (!aampClient)
3637
+ return;
3638
+ try {
3639
+ await aampClient.sendPairRespond({
3640
+ to: request.from,
3641
+ taskId: request.taskId,
3642
+ success,
3643
+ reason,
3644
+ inReplyTo: request.messageId
3645
+ });
3646
+ } catch (err) {
3647
+ api.logger.warn(`[AAMP] Failed to send pair.respond to ${request.from}: ${err.message}`);
3648
+ }
3649
+ }
3650
+ async function handlePairRequest(request) {
3651
+ if (!agentEmail)
3652
+ return;
3653
+ if (request.to.trim().toLowerCase() !== agentEmail.trim().toLowerCase())
3654
+ return;
3655
+ const consumed = consumePairingCode2({
3656
+ file: cfg.pairingFile ?? defaultPairingPath(),
3657
+ mailbox: agentEmail,
3658
+ pairCode: request.pairCode
3659
+ });
3660
+ if (!consumed) {
3661
+ const reason = "invalid or expired pair code";
3662
+ api.logger.warn(`[AAMP] Rejected pair.request from ${request.from}: ${reason}`);
3663
+ await sendPairResponse(request, false, reason);
3664
+ return;
3665
+ }
3666
+ pairedSenderPolicies = addPairedSenderPolicy(cfg.senderPoliciesFile ?? defaultSenderPoliciesPath(), {
3667
+ sender: request.from.trim().toLowerCase(),
3668
+ dispatchContextRules: request.dispatchContextRules ?? {},
3669
+ pairedAt: (/* @__PURE__ */ new Date()).toISOString()
3670
+ });
3671
+ api.logger.info(`[AAMP] Paired sender ${request.from}; sender policy saved to ${cfg.senderPoliciesFile ?? defaultSenderPoliciesPath()}`);
3672
+ await sendPairResponse(request, true);
3673
+ }
3674
+ async function renderPairingCodeForCurrentAgent() {
3675
+ const identity = agentEmail ? { email: agentEmail } : loadCachedIdentity(cfg.credentialsFile ?? defaultCredentialsPath());
3676
+ const email = identity?.email?.trim();
3677
+ if (!email) {
3678
+ return "Error: AAMP mailbox identity is not ready. Start the AAMP plugin service first.";
3679
+ }
3680
+ const pairing = createPairingCode2({
3681
+ mailbox: email,
3682
+ file: cfg.pairingFile ?? defaultPairingPath()
3683
+ });
3684
+ const qr = await renderTerminalQr(pairing.connectUrl);
3685
+ api.logger.info(`[AAMP] Pair with AAMP App before ${pairing.expiresAt}: ${pairing.connectUrl}`);
3686
+ if (qr)
3687
+ api.logger.info(`
3688
+ ${qr}`);
3689
+ return [
3690
+ `Pair ${email} with AAMP App or another AAMP runtime.`,
3691
+ `Expires: ${pairing.expiresAt}`,
3692
+ qr ? `
3693
+ Scan this QR code:
3694
+ ${qr}` : "\nCould not render a terminal QR code.",
3695
+ `
3696
+ Pairing URL: ${pairing.connectUrl}`
3697
+ ].join("\n");
3698
+ }
3109
3699
  function wakeAgentForPendingTask(task) {
3110
3700
  const fallbackSessionKey = buildWakeSessionKeyForPendingTask(task, api.config);
3111
3701
  const openClawSessionKey = buildSessionKeyForPendingTask(task, api.config);
@@ -3189,6 +3779,16 @@ var src_default = {
3189
3779
  lastTransportMode = "disconnected";
3190
3780
  lastLoggedTransportMode = "disconnected";
3191
3781
  api.logger.info(`[AAMP] Mailbox identity ready \u2014 ${agentEmail}`);
3782
+ pairedSenderPolicies = loadPairedSenderPolicies(cfg.senderPoliciesFile ?? defaultSenderPoliciesPath());
3783
+ const pairing = createPairingCode2({
3784
+ mailbox: agentEmail,
3785
+ file: cfg.pairingFile ?? defaultPairingPath()
3786
+ });
3787
+ api.logger.info(`[AAMP] Pair with AAMP App before ${pairing.expiresAt}: ${pairing.connectUrl}`);
3788
+ const qr = await renderTerminalQr(pairing.connectUrl);
3789
+ if (qr)
3790
+ api.logger.info(`
3791
+ ${qr}`);
3192
3792
  const base = baseUrl(cfg.aampHost);
3193
3793
  aampClient = AampClient.fromMailboxIdentity({
3194
3794
  email: identity.email,
@@ -3207,7 +3807,7 @@ var src_default = {
3207
3807
  api.logger.info(`[AAMP] Skipping already-terminal task ${task.taskId}`);
3208
3808
  return;
3209
3809
  }
3210
- const decision = matchSenderPolicy(task, cfg.senderPolicies);
3810
+ const decision = matchCombinedSenderPolicy(task, cfg.senderPolicies, pairedSenderPolicies);
3211
3811
  if (!decision.allowed) {
3212
3812
  api.logger.warn(`[AAMP] \u2717 rejected by senderPolicies: ${task.from} task=${task.taskId} reason=${decision.reason}`);
3213
3813
  void aampClient.sendResult({
@@ -3264,6 +3864,11 @@ var src_default = {
3264
3864
  api.logger.info(`[AAMP] Cancelled task ${cancel.taskId} \u2014 removed from pending queue`);
3265
3865
  }
3266
3866
  });
3867
+ aampClient.on("pair.request", (request) => {
3868
+ void handlePairRequest(request).catch((err) => {
3869
+ api.logger.warn(`[AAMP] Failed to handle pair.request: ${err.message}`);
3870
+ });
3871
+ });
3267
3872
  aampClient.on("task.result", (result) => {
3268
3873
  if (result.from.toLowerCase() === agentEmail.toLowerCase())
3269
3874
  return;
@@ -3331,7 +3936,6 @@ ${truncatedOutput}${attachmentInfo}` : `Agent ${result.from} rejected the sub-ta
3331
3936
 
3332
3937
  Reason: ${result.errorMsg ?? "unknown"}`,
3333
3938
  priority: "urgent",
3334
- contextLinks: [],
3335
3939
  messageId: "",
3336
3940
  receivedAt: (/* @__PURE__ */ new Date()).toISOString()
3337
3941
  });
@@ -3414,7 +4018,6 @@ Question: ${help.question}
3414
4018
  Blocked reason: ${help.blockedReason}${help.suggestedOptions?.length ? `
3415
4019
  Suggested options: ${help.suggestedOptions.join(", ")}` : ""}`,
3416
4020
  priority: "urgent",
3417
- contextLinks: [],
3418
4021
  messageId: "",
3419
4022
  receivedAt: (/* @__PURE__ */ new Date()).toISOString()
3420
4023
  });
@@ -3716,8 +4319,6 @@ ${Object.entries(task.dispatchContext).map(([key, value]) => ` - ${key}: ${valu
3716
4319
  task.threadContextText ? `${task.threadContextText}` : "",
3717
4320
  task.bodyText ? `Latest user message:
3718
4321
  ${task.bodyText}` : "",
3719
- task.contextLinks.length ? `Context Links:
3720
- ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
3721
4322
  task.expiresAt ? `Expires: ${task.expiresAt}` : `Expires: none`,
3722
4323
  `Received: ${task.receivedAt}`,
3723
4324
  otherActionableTasks.length > 0 ? `
@@ -3758,8 +4359,6 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
3758
4359
  task.threadContextText ? `${task.threadContextText}` : "",
3759
4360
  task.bodyText ? `Description:
3760
4361
  ${task.bodyText}` : "",
3761
- task.contextLinks.length ? `Context Links:
3762
- ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
3763
4362
  task.expiresAt ? `Expires: ${task.expiresAt}` : `Expires: none`,
3764
4363
  `Received: ${task.receivedAt}`,
3765
4364
  otherActionableTasks.length > 0 ? `
@@ -4081,6 +4680,14 @@ ${lines.join("\n")}`
4081
4680
  };
4082
4681
  }
4083
4682
  }, { name: "aamp_pending_tasks" });
4683
+ api.registerTool({
4684
+ name: "aamp_pairing_code",
4685
+ description: "Generate a fresh five-minute AAMP pairing code for this OpenClaw agent and show a QR code. Use this when the user asks to pair AAMP App or another AAMP runtime with this agent.",
4686
+ parameters: { type: "object", properties: {} },
4687
+ execute: async () => ({
4688
+ content: [{ type: "text", text: await renderPairingCodeForCurrentAgent() }]
4689
+ })
4690
+ }, { name: "aamp_pairing_code" });
4084
4691
  api.registerTool({
4085
4692
  name: "aamp_cancel_task",
4086
4693
  description: "Cancel a pending AAMP task and notify the dispatcher.",
@@ -4128,11 +4735,6 @@ ${lines.join("\n")}`
4128
4735
  parentTaskId: { type: "string", description: "If you are processing a pending AAMP task, pass its Task ID here to establish parent-child nesting. Omit for top-level tasks." },
4129
4736
  priority: { type: "string", enum: ["urgent", "high", "normal"], description: "Task priority (optional)" },
4130
4737
  expiresAt: { type: "string", description: "Absolute expiry time in ISO 8601 format (optional)" },
4131
- contextLinks: {
4132
- type: "array",
4133
- items: { type: "string" },
4134
- description: "URLs providing context (optional)"
4135
- },
4136
4738
  attachments: {
4137
4739
  type: "array",
4138
4740
  description: "File attachments. Each item: { filename, contentType, path (local file path) }",
@@ -4167,7 +4769,6 @@ ${lines.join("\n")}`
4167
4769
  parentTaskId: params.parentTaskId,
4168
4770
  priority: params.priority,
4169
4771
  expiresAt: params.expiresAt,
4170
- contextLinks: params.contextLinks,
4171
4772
  attachments
4172
4773
  });
4173
4774
  dispatchedSubtasks.set(result.taskId, {
@@ -4348,6 +4949,15 @@ Question: ${h.question}`,
4348
4949
  };
4349
4950
  }
4350
4951
  });
4952
+ api.registerCommand({
4953
+ name: "aamp-pair",
4954
+ description: "Show a fresh AAMP pairing QR code for this OpenClaw agent",
4955
+ acceptsArgs: false,
4956
+ requireAuth: false,
4957
+ handler: async () => ({
4958
+ text: await renderPairingCodeForCurrentAgent()
4959
+ })
4960
+ });
4351
4961
  }
4352
4962
  };
4353
4963
  export {