@agentcomms/resend 0.12.2 → 0.12.3
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/cli.mjs +63 -18
- package/dist/index.d.mts +9 -4
- package/dist/index.mjs +2 -2
- package/dist/{send-BlDpMIlS.mjs → send-BedjeO_M.mjs} +712 -509
- package/dist/{server-B-ubuD7Z.mjs → server-Bq2OYONI.mjs} +3 -2
- package/dist/{stdio-entry-Cibag7m5.mjs → stdio-entry-QIUMjquj.mjs} +1 -1
- package/package.json +3 -3
|
@@ -4,15 +4,15 @@ import path, { basename, delimiter, dirname, extname, isAbsolute, join, relative
|
|
|
4
4
|
import { domainToASCII, fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { createHash, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
|
|
6
6
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
7
|
-
import { homedir, platform, userInfo } from "node:os";
|
|
8
|
-
import { constants as constants$1, realpathSync } from "node:fs";
|
|
9
7
|
import "node:readline/promises";
|
|
10
8
|
import { styleText } from "node:util";
|
|
9
|
+
import { homedir, platform, userInfo } from "node:os";
|
|
10
|
+
import { constants as constants$1, realpathSync } from "node:fs";
|
|
11
11
|
import { execFile, spawn } from "node:child_process";
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region src/version.ts
|
|
14
14
|
/** The package version, read from package.json at build time. */
|
|
15
|
-
const VERSION$1 = "0.12.
|
|
15
|
+
const VERSION$1 = "0.12.3";
|
|
16
16
|
//#endregion
|
|
17
17
|
//#region src/api/routes.ts
|
|
18
18
|
/**
|
|
@@ -187,7 +187,7 @@ function refusedBecause(method, pathname) {
|
|
|
187
187
|
return null;
|
|
188
188
|
}
|
|
189
189
|
//#endregion
|
|
190
|
-
//#region ../core/dist/update-check-
|
|
190
|
+
//#region ../core/dist/update-check-Dq-D4bAD.mjs
|
|
191
191
|
/**
|
|
192
192
|
* Every failure a user or an agent can act on carries a specific, stable code from this registry, a message, an
|
|
193
193
|
* optional one-line hint and structured `details`. The code decides the process exit status; agents and skills branch
|
|
@@ -1211,6 +1211,253 @@ function connectMailboxCommand() {
|
|
|
1211
1211
|
const mail = accountChannels().find((manifest) => manifest.accounts?.map === "inboxes");
|
|
1212
1212
|
return mail ? `${mail.binary} inbox add` : void 0;
|
|
1213
1213
|
}
|
|
1214
|
+
/** Crockford-style alphabet without I, L, O and U, so ids and challenges survive being read aloud or retyped. */
|
|
1215
|
+
const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
1216
|
+
function randomString(length) {
|
|
1217
|
+
const bytes = randomBytes(length);
|
|
1218
|
+
let out = "";
|
|
1219
|
+
for (let i = 0; i < length; i += 1) out += ALPHABET[(bytes[i] ?? 0) & 31];
|
|
1220
|
+
return out;
|
|
1221
|
+
}
|
|
1222
|
+
/** `ap_` + 26 characters: 130 random bits. Validated before an id ever names a file. */
|
|
1223
|
+
const APPROVAL_ID_PATTERN = /^ap_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
1224
|
+
const PLAN_TOKEN_PATTERN = /^pl_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
1225
|
+
function newApprovalId() {
|
|
1226
|
+
return `ap_${randomString(26)}`;
|
|
1227
|
+
}
|
|
1228
|
+
function newPlanToken() {
|
|
1229
|
+
return `pl_${randomString(26)}`;
|
|
1230
|
+
}
|
|
1231
|
+
const CHALLENGE_LETTERS = "ABCDEFGHJKMNPQRSTVWXYZ";
|
|
1232
|
+
/** A short challenge a human types back to approve: letters only, no look-alikes. */
|
|
1233
|
+
function newChallenge(length = 4) {
|
|
1234
|
+
let out = "";
|
|
1235
|
+
for (let i = 0; i < length; i += 1) out += CHALLENGE_LETTERS[randomInt(22)];
|
|
1236
|
+
return out;
|
|
1237
|
+
}
|
|
1238
|
+
/** Challenges are stored only as hashes, so no listing or tool result can reveal one. */
|
|
1239
|
+
function hashChallenge(challenge) {
|
|
1240
|
+
return createHash("sha256").update(challenge.trim().toUpperCase()).digest("hex");
|
|
1241
|
+
}
|
|
1242
|
+
/** Case-insensitive, constant-time comparison of an answer with a stored challenge hash. */
|
|
1243
|
+
function challengeMatches(answer, storedHash) {
|
|
1244
|
+
const a = Buffer.from(hashChallenge(answer), "hex");
|
|
1245
|
+
const b = Buffer.from(storedHash, "hex");
|
|
1246
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
1247
|
+
}
|
|
1248
|
+
function okEnvelope(data) {
|
|
1249
|
+
return {
|
|
1250
|
+
ok: true,
|
|
1251
|
+
schemaVersion: 1,
|
|
1252
|
+
data
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
function errorEnvelope(error) {
|
|
1256
|
+
const body = {
|
|
1257
|
+
code: error.code,
|
|
1258
|
+
message: error.message
|
|
1259
|
+
};
|
|
1260
|
+
if (error.hint !== void 0) body.hint = error.hint;
|
|
1261
|
+
if (error.details !== void 0) body.details = error.details;
|
|
1262
|
+
return {
|
|
1263
|
+
ok: false,
|
|
1264
|
+
schemaVersion: 1,
|
|
1265
|
+
error: body
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
const defaultStreams = {
|
|
1269
|
+
stdout: process.stdout,
|
|
1270
|
+
stderr: process.stderr,
|
|
1271
|
+
stdin: process.stdin
|
|
1272
|
+
};
|
|
1273
|
+
function colorEnabled(env, stream, flag) {
|
|
1274
|
+
if (flag === false) return false;
|
|
1275
|
+
if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return false;
|
|
1276
|
+
if (env.TERM === "dumb") return false;
|
|
1277
|
+
if (env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "0") return true;
|
|
1278
|
+
return Boolean(stream.isTTY);
|
|
1279
|
+
}
|
|
1280
|
+
/** True when a human could answer a prompt: both ends are terminals, no --json/--no-input, not CI. */
|
|
1281
|
+
function canPrompt(env, streams, options) {
|
|
1282
|
+
if (options.json || options.noInput) return false;
|
|
1283
|
+
if (env.CI && env.CI !== "0" && env.CI !== "false") return false;
|
|
1284
|
+
return Boolean(streams.stdin?.isTTY && streams.stdout.isTTY);
|
|
1285
|
+
}
|
|
1286
|
+
/** Environment variables well-known coding agents set. Used only as a speed bump, never as a security boundary. */
|
|
1287
|
+
const AGENT_MARKERS = [
|
|
1288
|
+
"CLAUDECODE",
|
|
1289
|
+
"CLAUDE_CODE_ENTRYPOINT",
|
|
1290
|
+
"CODEX_SANDBOX",
|
|
1291
|
+
"CODEX_HOME",
|
|
1292
|
+
"CURSOR_AGENT",
|
|
1293
|
+
"GEMINI_CLI",
|
|
1294
|
+
"AGENT_COMMS_AGENT"
|
|
1295
|
+
];
|
|
1296
|
+
function agentMarker(env) {
|
|
1297
|
+
for (const name of AGENT_MARKERS) if (env[name] !== void 0 && env[name] !== "") return name;
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
function paint(color, format, text) {
|
|
1301
|
+
return color ? styleText(format, text, { validateStream: false }) : text;
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* A command line for a person to copy and run, each word quoted only where the shell it is pasted into would need it.
|
|
1305
|
+
* Every command this package prints to be run — a change to run again with its approval, a folder to take out, an
|
|
1306
|
+
* entry to register again or remove — is quoted here, so no printer quotes for a shell of its own.
|
|
1307
|
+
*
|
|
1308
|
+
* Everywhere but Windows that shell is a POSIX one, and a word goes in single quotes, inside which nothing is special
|
|
1309
|
+
* but the quote itself. Windows has two shells, and neither reads single quotes that way: cmd.exe does not take them
|
|
1310
|
+
* as quotes at all, so `'C:\Profiles\First Last\outgoing'` reached the command as two words, quote marks and all, and
|
|
1311
|
+
* PowerShell does, but escapes a quote inside them by doubling it rather than as `'\''`. A command printed on Windows
|
|
1312
|
+
* has to be safe in both, because nothing says which one it will be pasted into (CUE-306) — and safe is not enough:
|
|
1313
|
+
* the program has to receive the same words from either. Three readers stand between the line and the program. cmd.exe
|
|
1314
|
+
* hands the program the line as it is, and the program's own parser (the C runtime's, Node's) splits it. PowerShell
|
|
1315
|
+
* reads the line itself, a double-quoted word with its backslashes as plain characters, and then writes a new command
|
|
1316
|
+
* line for the program: Windows PowerShell 5.1 the old way ("Legacy" in about_Parsing, "Passing arguments to native
|
|
1317
|
+
* applications") — a word quoted only when it holds whitespace, as it is, and an empty word dropped — and PowerShell
|
|
1318
|
+
* 7.3 and later its own way ("Standard") — quoted when it must be, with every backslash before a quote doubled. For a
|
|
1319
|
+
* `.cmd` script, which is how npm installs `agentcomms`, `claude` and `codex` on Windows, PowerShell 7 goes back to the
|
|
1320
|
+
* old way, and cmd.exe then reads that new line. So on Windows:
|
|
1321
|
+
*
|
|
1322
|
+
* - A word of letters, digits and `_ + = : . / \`, with `@` and `-` anywhere but first, is left as it is: no reader
|
|
1323
|
+
* does anything with it, and a backslash is an ordinary character to all of them, at the end too. A first `@` is
|
|
1324
|
+
* splatting to PowerShell, and a `,` its array operator — two words — so a word with either is quoted. So is a word
|
|
1325
|
+
* that starts with a digit, which PowerShell may read as a number (`1kb`, `0x10`), and one that starts with `-` but
|
|
1326
|
+
* is not a plain option, which PowerShell may read as a parameter of its own and split (`-name.x`).
|
|
1327
|
+
* - A word that double quotes bring through all three readers whole goes in double quotes. Inside them cmd.exe reads
|
|
1328
|
+
* `& | < > ^ ( )` and spaces as ordinary characters, and PowerShell reads everything as ordinary but `$`, the
|
|
1329
|
+
* backtick and a double quote. What is left special in one or the other is kept out: a double quote, which ends the
|
|
1330
|
+
* quoting in both — and PowerShell takes the curly ones, `“ ” „`, for one too; `$` and the backtick, PowerShell's
|
|
1331
|
+
* expansion and escape; `%`, which cmd.exe expands as `%NAME%` before it looks at quotes at all; `!`, which it
|
|
1332
|
+
* expands as `!NAME!` inside quotes too wherever delayed expansion is on, and which then makes a `^` inside quotes an
|
|
1333
|
+
* escape; and any control or formatting character — a line break ends the command in cmd.exe even inside quotes, a
|
|
1334
|
+
* tab pasted into cmd.exe can complete a file name, and a right-to-left override shows a line other than the one
|
|
1335
|
+
* that runs. Three more cannot come through: an empty word, which Windows PowerShell drops; a word ending in a
|
|
1336
|
+
* backslash, which the C runtime reads with the closing quote as `\"` — and doubled for it, PowerShell 7 passes both
|
|
1337
|
+
* backslashes on where cmd.exe and Windows PowerShell pass one; and a word with `& | < > ^ ( )` but no whitespace,
|
|
1338
|
+
* which PowerShell passes to a `.cmd` script unquoted, for cmd.exe to run `&whoami` from or to split at `|`.
|
|
1339
|
+
* - With any other word in it, no line is printed at all (`line` is null), and the printers show the command's words
|
|
1340
|
+
* as JSON instead, saying it has to be typed. Not a line with the word left out: a placeholder in its place was
|
|
1341
|
+
* still a command that ran — `claude mcp remove NAME` removed whatever entry was called `NAME`, and an install
|
|
1342
|
+
* hint's `--force` replaced it — and before that, a word quoted for PowerShell's single quotes, which cmd.exe reads
|
|
1343
|
+
* as characters, ran `whoami` from a server named `$x&whoami&`. See `commandAsJson` for why the JSON runs nothing.
|
|
1344
|
+
*
|
|
1345
|
+
* Everywhere else there is always a line: single quotes make any word safe.
|
|
1346
|
+
*/
|
|
1347
|
+
function shellCommand(words, platform = process.platform) {
|
|
1348
|
+
if (platform !== "win32") return {
|
|
1349
|
+
words,
|
|
1350
|
+
line: words.map(posixShellWord).join(" "),
|
|
1351
|
+
platform
|
|
1352
|
+
};
|
|
1353
|
+
const printed = words.map(windowsShellWord);
|
|
1354
|
+
return {
|
|
1355
|
+
words,
|
|
1356
|
+
line: printed.includes(null) ? null : printed.join(" "),
|
|
1357
|
+
platform
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
/** The same command with more words at its end — the approval it is to be run again with — for the same shell. */
|
|
1361
|
+
function withWords(command, ...more) {
|
|
1362
|
+
return shellCommand([...command.words, ...more], command.platform);
|
|
1363
|
+
}
|
|
1364
|
+
function posixShellWord(word) {
|
|
1365
|
+
return /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`;
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* The word as cmd.exe, Windows PowerShell and PowerShell 7 all hand it to a program, or null when no printing of it
|
|
1369
|
+
* does (see `shellCommand`). Bare: a plain option, or ordinary characters that do not start like a number.
|
|
1370
|
+
*/
|
|
1371
|
+
function windowsShellWord(word) {
|
|
1372
|
+
if (/^(?:--?[A-Za-z][A-Za-z0-9-]*|(?!\+?\.?\d)[\w+=:./\\][\w@+=:./\\-]*)$/.test(word)) return word;
|
|
1373
|
+
if (word === "" || word.endsWith("\\")) return null;
|
|
1374
|
+
if (/["$`%!\u201C-\u201E]|[\p{C}\p{Zl}\p{Zp}]/u.test(word)) return null;
|
|
1375
|
+
if (/[&|<>^()]/.test(word) && !/\s/.test(word)) return null;
|
|
1376
|
+
return `"${word}"`;
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* The words of a command as a JSON array, for a command that cannot be printed as a line: something to read, and to
|
|
1380
|
+
* parse, and nothing to run.
|
|
1381
|
+
*
|
|
1382
|
+
* Pasted into either Windows shell by mistake, it runs nothing. PowerShell refuses it before running anything: a `[`
|
|
1383
|
+
* opens a type name, and a quoted string is none. cmd.exe looks for a program called `["agentcomms"` or the like, and
|
|
1384
|
+
* finds none — and nothing in the line is anything else to it: every character it acts on, inside quotes or out (`%`,
|
|
1385
|
+
* `!`), is written as a `\u` escape, and so is every double quote inside a word, so the quotes cmd.exe sees are the
|
|
1386
|
+
* JSON's own, in pairs, and `&`, `|`, `<`, `>`, `^` and the parentheses are only ever inside them, where it reads them
|
|
1387
|
+
* as characters. `$` and the backtick are escaped too, so that PowerShell would expand nothing even if it read on, and
|
|
1388
|
+
* so is everything outside printable ASCII — a line break, a curly quote, a right-to-left override. A backslash is
|
|
1389
|
+
* doubled, as JSON has it.
|
|
1390
|
+
*/
|
|
1391
|
+
function commandAsJson(words) {
|
|
1392
|
+
const word = (text) => {
|
|
1393
|
+
let json = "";
|
|
1394
|
+
for (let index = 0; index < text.length; index++) {
|
|
1395
|
+
const code = text.charCodeAt(index);
|
|
1396
|
+
const char = text[index];
|
|
1397
|
+
if (char === "\\") json += "\\\\";
|
|
1398
|
+
else if (code >= 32 && code <= 126 && !"\"%!$`".includes(char)) json += char;
|
|
1399
|
+
else json += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
1400
|
+
}
|
|
1401
|
+
return `"${json}"`;
|
|
1402
|
+
};
|
|
1403
|
+
return `[${words.map(word).join(",")}]`;
|
|
1404
|
+
}
|
|
1405
|
+
/** What is said beside a command shown as JSON. */
|
|
1406
|
+
const TO_TYPE = "the command's words, written as JSON: one of them cannot be quoted the same way for cmd.exe and for PowerShell, so type the command yourself, with that word quoted for the shell you use";
|
|
1407
|
+
/** A command in backticks, for a sentence — as its words in JSON, saying it has to be typed, when it has no line. */
|
|
1408
|
+
function inlineCommand(command) {
|
|
1409
|
+
return command.line === null ? `\`${commandAsJson(command.words)}\` (${TO_TYPE})` : `\`${command.line}\``;
|
|
1410
|
+
}
|
|
1411
|
+
/** A command as text of its own — a list's line, a field's value — or its words in JSON, saying it has to be typed. */
|
|
1412
|
+
function commandText(command) {
|
|
1413
|
+
return command.line === null ? `${commandAsJson(command.words)} (${TO_TYPE})` : command.line;
|
|
1414
|
+
}
|
|
1415
|
+
/** Writes a successful result: the envelope with --json, otherwise the human rendering. */
|
|
1416
|
+
function writeResult(data, options, human, streams = defaultStreams) {
|
|
1417
|
+
if (options.json) {
|
|
1418
|
+
streams.stdout.write(`${JSON.stringify(okEnvelope(data))}\n`);
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
const text = human(data);
|
|
1422
|
+
if (text) streams.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
1423
|
+
}
|
|
1424
|
+
/** Writes an error and returns the exit code to use. */
|
|
1425
|
+
function writeError(error, options, streams = defaultStreams) {
|
|
1426
|
+
const commsError = toCommsError(error);
|
|
1427
|
+
if (options.json) streams.stdout.write(`${JSON.stringify(errorEnvelope(commsError))}\n`);
|
|
1428
|
+
else {
|
|
1429
|
+
streams.stderr.write(`${paint(options.color, "red", "error")}: ${commsError.message}\n`);
|
|
1430
|
+
if (commsError.hint) streams.stderr.write(`${paint(options.color, "dim", "hint")}: ${commsError.hint}\n`);
|
|
1431
|
+
}
|
|
1432
|
+
return commsError.exitCode;
|
|
1433
|
+
}
|
|
1434
|
+
/** Runs a command body and exits with the documented code. Unexpected errors keep only their message. */
|
|
1435
|
+
async function runCommand(options, body, streams = defaultStreams) {
|
|
1436
|
+
try {
|
|
1437
|
+
await body();
|
|
1438
|
+
return EXIT_CODES.OK;
|
|
1439
|
+
} catch (error) {
|
|
1440
|
+
return writeError(error, options, streams);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* The first two steps of `requirePerson` — an agent is refused, then anything without a terminal — for a command
|
|
1445
|
+
* whose code is not its own to make up.
|
|
1446
|
+
*
|
|
1447
|
+
* `agentcomms approve` asks for a code the approval store issued and will check, so it cannot use `askChallenge`;
|
|
1448
|
+
* but it refuses in exactly the same order and words, because it is the same gate.
|
|
1449
|
+
*/
|
|
1450
|
+
function refuseUnlessPerson(env, streams, gate) {
|
|
1451
|
+
const marker = agentMarker(env);
|
|
1452
|
+
if (marker) throw new CommsError("LOOSENING_REFUSED", gate.refusedToAgent, {
|
|
1453
|
+
hint: `Ask the user to run \`${gate.command}\` in their own terminal.`,
|
|
1454
|
+
details: { marker }
|
|
1455
|
+
});
|
|
1456
|
+
const prompting = {};
|
|
1457
|
+
if (gate.json !== void 0) prompting.json = gate.json;
|
|
1458
|
+
if (gate.noInput !== void 0) prompting.noInput = gate.noInput;
|
|
1459
|
+
if (!canPrompt(env, streams, prompting)) throw new CommsError("LOOSENING_REFUSED", gate.refusedWithoutTerminal, { hint: `Run \`${gate.command}\` directly in a terminal.` });
|
|
1460
|
+
}
|
|
1214
1461
|
/**
|
|
1215
1462
|
* One table of dangerous characters, shared by the inbound sanitiser (which strips them from sender-controlled text)
|
|
1216
1463
|
* and the preview renderer (which makes them visible), so the two can never disagree.
|
|
@@ -1871,13 +2118,19 @@ function committedSecretsStore(config) {
|
|
|
1871
2118
|
* A different backend asked for is refused, not taken: one backend holds everything here, and changing it has to move
|
|
1872
2119
|
* what is already stored, which is what `agentcomms secrets migrate` does and nothing else does.
|
|
1873
2120
|
*/
|
|
1874
|
-
function secretsStoreFor(config, requested) {
|
|
2121
|
+
function secretsStoreFor(config, requested, platform = process.platform) {
|
|
1875
2122
|
const committed = committedSecretsStore(config);
|
|
1876
2123
|
if (committed === null) return {
|
|
1877
2124
|
store: requested ?? "keychain",
|
|
1878
2125
|
choosing: true
|
|
1879
2126
|
};
|
|
1880
|
-
if (requested !== void 0 && requested !== committed) throw new CommsError("CONFIG", `this configuration already keeps its secrets in the ${committed} store`, { hint: `Everything here uses one store, and changing it moves what is already stored. To change it, run
|
|
2127
|
+
if (requested !== void 0 && requested !== committed) throw new CommsError("CONFIG", `this configuration already keeps its secrets in the ${committed} store`, { hint: `Everything here uses one store, and changing it moves what is already stored. To change it, run ${inlineCommand(shellCommand([
|
|
2128
|
+
"agentcomms",
|
|
2129
|
+
"secrets",
|
|
2130
|
+
"migrate",
|
|
2131
|
+
"--to",
|
|
2132
|
+
requested
|
|
2133
|
+
], platform))}, then run this again.` });
|
|
1881
2134
|
return {
|
|
1882
2135
|
store: committed,
|
|
1883
2136
|
choosing: false
|
|
@@ -2420,40 +2673,6 @@ function canonicalLoosening(loosening) {
|
|
|
2420
2673
|
function sameLoosening(a, b) {
|
|
2421
2674
|
return canonicalLoosening(a) === canonicalLoosening(b);
|
|
2422
2675
|
}
|
|
2423
|
-
/** Crockford-style alphabet without I, L, O and U, so ids and challenges survive being read aloud or retyped. */
|
|
2424
|
-
const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
2425
|
-
function randomString(length) {
|
|
2426
|
-
const bytes = randomBytes(length);
|
|
2427
|
-
let out = "";
|
|
2428
|
-
for (let i = 0; i < length; i += 1) out += ALPHABET[(bytes[i] ?? 0) & 31];
|
|
2429
|
-
return out;
|
|
2430
|
-
}
|
|
2431
|
-
/** `ap_` + 26 characters: 130 random bits. Validated before an id ever names a file. */
|
|
2432
|
-
const APPROVAL_ID_PATTERN = /^ap_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
2433
|
-
const PLAN_TOKEN_PATTERN = /^pl_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
2434
|
-
function newApprovalId() {
|
|
2435
|
-
return `ap_${randomString(26)}`;
|
|
2436
|
-
}
|
|
2437
|
-
function newPlanToken() {
|
|
2438
|
-
return `pl_${randomString(26)}`;
|
|
2439
|
-
}
|
|
2440
|
-
const CHALLENGE_LETTERS = "ABCDEFGHJKMNPQRSTVWXYZ";
|
|
2441
|
-
/** A short challenge a human types back to approve: letters only, no look-alikes. */
|
|
2442
|
-
function newChallenge(length = 4) {
|
|
2443
|
-
let out = "";
|
|
2444
|
-
for (let i = 0; i < length; i += 1) out += CHALLENGE_LETTERS[randomInt(22)];
|
|
2445
|
-
return out;
|
|
2446
|
-
}
|
|
2447
|
-
/** Challenges are stored only as hashes, so no listing or tool result can reveal one. */
|
|
2448
|
-
function hashChallenge(challenge) {
|
|
2449
|
-
return createHash("sha256").update(challenge.trim().toUpperCase()).digest("hex");
|
|
2450
|
-
}
|
|
2451
|
-
/** Case-insensitive, constant-time comparison of an answer with a stored challenge hash. */
|
|
2452
|
-
function challengeMatches(answer, storedHash) {
|
|
2453
|
-
const a = Buffer.from(hashChallenge(answer), "hex");
|
|
2454
|
-
const b = Buffer.from(storedHash, "hex");
|
|
2455
|
-
return a.length === b.length && timingSafeEqual(a, b);
|
|
2456
|
-
}
|
|
2457
2676
|
/**
|
|
2458
2677
|
* The digest a download's question is bound to: the account, the download, the request, the files it listed and the
|
|
2459
2678
|
* names it showed them under.
|
|
@@ -2747,19 +2966,23 @@ var ApprovalStore = class {
|
|
|
2747
2966
|
* Nothing written, because the caller made a mistake about an approval that may be perfectly good: voiding a post
|
|
2748
2967
|
* somebody is about to approve, because an agent passed its id to a change, would punish the wrong party.
|
|
2749
2968
|
*/
|
|
2750
|
-
#requireKind(record, kind) {
|
|
2969
|
+
#requireKind(record, kind, platform) {
|
|
2751
2970
|
const actual = approvalKind(record);
|
|
2752
2971
|
if (actual === kind) return;
|
|
2753
2972
|
const id = record.approvalId;
|
|
2754
2973
|
if (actual === "download") throw (kind === "change" ? refuseChange : refuse$1)("USAGE", `${id} is a question about where to save files, not ${kind === "send" ? "a send" : "a configuration change"}`, record, DOWNLOAD_ANSWER_HINT);
|
|
2755
2974
|
if (kind === "download") throw refuseDownload("USAGE", `${id} is ${actual === "change" ? "a configuration change" : "a send"}, not a question about where to save files`, record, "Pass the choice id the download’s question came with, and the person’s answer beside it.");
|
|
2756
|
-
throw kind === "send" ? refuse$1("USAGE", `approval ${id} is for a configuration change, not a send`, record, `A person approves it with
|
|
2975
|
+
throw kind === "send" ? refuse$1("USAGE", `approval ${id} is for a configuration change, not a send`, record, `A person approves it with ${inlineCommand(shellCommand([
|
|
2976
|
+
"agentcomms",
|
|
2977
|
+
"approve",
|
|
2978
|
+
id
|
|
2979
|
+
], platform))} — or ${channelApproveCommands()}, whichever is installed — and it permits only the change it was prepared for.`) : refuseChange("USAGE", `approval ${id} is for a send, not a configuration change`, record, `It is approved with the command that prepared it — ${channelApproveCommands({ sending: true })} — and permits only that send.`);
|
|
2757
2980
|
}
|
|
2758
2981
|
/** Issues a new challenge to show a human; only its hash is kept. */
|
|
2759
|
-
async issueChallenge(approvalId, kind = "send") {
|
|
2982
|
+
async issueChallenge(approvalId, kind = "send", platform = process.platform) {
|
|
2760
2983
|
const challenge = newChallenge();
|
|
2761
2984
|
await this.#transition(approvalId, (current) => {
|
|
2762
|
-
this.#requireKind(current, kind);
|
|
2985
|
+
this.#requireKind(current, kind, platform);
|
|
2763
2986
|
if (current.state !== "pending") throw this.#stateError(current);
|
|
2764
2987
|
return {
|
|
2765
2988
|
...current,
|
|
@@ -2776,10 +2999,10 @@ var ApprovalStore = class {
|
|
|
2776
2999
|
* A change is approved the same way, with `kind: 'change'` and its digest standing in for the draft (see
|
|
2777
3000
|
* `createChange`), so a person's typed code means one thing whichever kind of approval it is typed for.
|
|
2778
3001
|
*/
|
|
2779
|
-
async approve(approvalId, via, live, answer, kind = "send") {
|
|
3002
|
+
async approve(approvalId, via, live, answer, kind = "send", platform = process.platform) {
|
|
2780
3003
|
let failure = null;
|
|
2781
3004
|
const result = await this.#transition(approvalId, (current) => {
|
|
2782
|
-
this.#requireKind(current, kind);
|
|
3005
|
+
this.#requireKind(current, kind, platform);
|
|
2783
3006
|
if (current.state !== "pending") throw this.#stateError(current);
|
|
2784
3007
|
if (!current.challengeHash) throw refusalFor(current)("APPROVAL_REQUIRED", "no challenge was issued for this approval", current);
|
|
2785
3008
|
if (live.draftMessageId !== current.draftMessageId || live.digest !== current.digest) {
|
|
@@ -2836,7 +3059,7 @@ var ApprovalStore = class {
|
|
|
2836
3059
|
async claimForSend(approvalId, live, options = {}) {
|
|
2837
3060
|
let failure = null;
|
|
2838
3061
|
const result = await this.#transition(approvalId, (current) => {
|
|
2839
|
-
this.#requireKind(current, "send");
|
|
3062
|
+
this.#requireKind(current, "send", options.platform ?? process.platform);
|
|
2840
3063
|
if (current.state !== "pending" && current.state !== "approved") throw this.#stateError(current);
|
|
2841
3064
|
if (options.signal?.aborted) throw cancelledClaim(current);
|
|
2842
3065
|
const voidWith = (code, reason) => {
|
|
@@ -2947,7 +3170,7 @@ var ApprovalStore = class {
|
|
|
2947
3170
|
const digest = changeDigest(live.change);
|
|
2948
3171
|
let failure = null;
|
|
2949
3172
|
const result = await this.#transition(approvalId, (current) => {
|
|
2950
|
-
this.#requireKind(current, "change");
|
|
3173
|
+
this.#requireKind(current, "change", options.platform ?? process.platform);
|
|
2951
3174
|
if (current.state !== "pending" && current.state !== "approved") throw this.#stateError(current);
|
|
2952
3175
|
if (options.signal?.aborted) throw cancelledClaim(current);
|
|
2953
3176
|
const voidWith = (reason) => {
|
|
@@ -2963,7 +3186,11 @@ var ApprovalStore = class {
|
|
|
2963
3186
|
};
|
|
2964
3187
|
if (digest !== current.digest) return voidWith(current.change ? changeDrift(current.change, live.change) : "the change is not the one that was approved");
|
|
2965
3188
|
if (stricterPolicy(live.policy, current.requiredPolicy) !== "chat") {
|
|
2966
|
-
if (current.state !== "approved") throw refuseChange("APPROVAL_PENDING", "this change needs a person to approve it at a terminal first", current, options.pendingHint ?? `Ask the user to run
|
|
3189
|
+
if (current.state !== "approved") throw refuseChange("APPROVAL_PENDING", "this change needs a person to approve it at a terminal first", current, options.pendingHint ?? `Ask the user to run ${inlineCommand(shellCommand([
|
|
3190
|
+
"agentcomms",
|
|
3191
|
+
"approve",
|
|
3192
|
+
approvalId
|
|
3193
|
+
], options.platform ?? process.platform))} in their own terminal, then try again with the same approval.`);
|
|
2967
3194
|
if (current.approvedVia !== "terminal") return voidWith("the change policy is confirm, and this was not approved at a terminal");
|
|
2968
3195
|
}
|
|
2969
3196
|
return {
|
|
@@ -3040,13 +3267,13 @@ var ApprovalStore = class {
|
|
|
3040
3267
|
*
|
|
3041
3268
|
* Only a pending question can be answered, and only once: a second answer is refused, not taken over the first.
|
|
3042
3269
|
*/
|
|
3043
|
-
async answerDownload(approvalId, via, answer) {
|
|
3270
|
+
async answerDownload(approvalId, via, answer, platform = process.platform) {
|
|
3044
3271
|
const recorded = answer.choice === "other" ? {
|
|
3045
3272
|
choice: "other",
|
|
3046
3273
|
folder: answer.folder
|
|
3047
3274
|
} : { choice: answer.choice };
|
|
3048
3275
|
return await this.#transition(approvalId, (current) => {
|
|
3049
|
-
this.#requireKind(current, "download");
|
|
3276
|
+
this.#requireKind(current, "download", platform);
|
|
3050
3277
|
if (current.state === "approved") throw refuseDownload("APPROVAL_VOID", "the question was answered already, and it is answered once", current);
|
|
3051
3278
|
if (current.state !== "pending") throw this.#stateError(current);
|
|
3052
3279
|
if (!current.download || downloadDigest(current.download) !== current.digest) throw refuseDownload("APPROVAL_VOID", "the question does not describe the download it is bound to", current);
|
|
@@ -3085,7 +3312,7 @@ var ApprovalStore = class {
|
|
|
3085
3312
|
const digest = downloadDigest(live);
|
|
3086
3313
|
let failure = null;
|
|
3087
3314
|
const result = await this.#transition(approvalId, (current) => {
|
|
3088
|
-
this.#requireKind(current, "download");
|
|
3315
|
+
this.#requireKind(current, "download", options.platform ?? process.platform);
|
|
3089
3316
|
if (current.state !== "pending" && current.state !== "approved") throw this.#stateError(current);
|
|
3090
3317
|
if (options.signal?.aborted) throw cancelledClaim(current);
|
|
3091
3318
|
const voidWith = (reason) => {
|
|
@@ -3244,330 +3471,117 @@ const MAP = {
|
|
|
3244
3471
|
* An own property only.
|
|
3245
3472
|
*
|
|
3246
3473
|
* A name is user input and the maps are plain objects, so `config.inboxes.constructor` is a function — and a lookup
|
|
3247
|
-
* of an inbox called `constructor`, which version 1 allows, found it.
|
|
3248
|
-
*/
|
|
3249
|
-
function own(map, key) {
|
|
3250
|
-
return Object.hasOwn(map, key) ? map[key] : void 0;
|
|
3251
|
-
}
|
|
3252
|
-
function resolveName(config, kind, name, notFound) {
|
|
3253
|
-
if (kind === "inbox") {
|
|
3254
|
-
const inbox = own(config.inboxes, name);
|
|
3255
|
-
if (inbox) return {
|
|
3256
|
-
alias: name,
|
|
3257
|
-
inbox
|
|
3258
|
-
};
|
|
3259
|
-
} else {
|
|
3260
|
-
const account = own(config.accounts, name);
|
|
3261
|
-
if (account) return {
|
|
3262
|
-
alias: name,
|
|
3263
|
-
account
|
|
3264
|
-
};
|
|
3265
|
-
}
|
|
3266
|
-
const renamed = formerNameRefusal(config, kind, name);
|
|
3267
|
-
if (renamed) throw renamed;
|
|
3268
|
-
throw notFound?.() ?? defaultNotFound(config, kind, name);
|
|
3269
|
-
}
|
|
3270
|
-
/** What to say when there are no mailboxes at all: the command that connects one, from the mail channel's manifest. */
|
|
3271
|
-
function noInboxesHint() {
|
|
3272
|
-
const connect = connectMailboxCommand();
|
|
3273
|
-
return connect ? `No inboxes yet: add one with \`${connect}\`.` : "No inboxes yet.";
|
|
3274
|
-
}
|
|
3275
|
-
function defaultNotFound(config, kind, name) {
|
|
3276
|
-
if (kind === "inbox") {
|
|
3277
|
-
const known = Object.keys(config.inboxes);
|
|
3278
|
-
return new CommsError("NOT_FOUND", `no inbox called "${name}"`, { hint: known.length ? `Known inboxes: ${known.join(", ")}.` : noInboxesHint() });
|
|
3279
|
-
}
|
|
3280
|
-
return new CommsError("NOT_FOUND", `no account called "${name}"`);
|
|
3281
|
-
}
|
|
3282
|
-
function lookupName(config, kind, name) {
|
|
3283
|
-
return kind === "inbox" ? own(config.inboxes, name) : own(config.accounts, name);
|
|
3284
|
-
}
|
|
3285
|
-
function findById(config, kind, id) {
|
|
3286
|
-
if (kind === "inbox") {
|
|
3287
|
-
for (const [alias, inbox] of Object.entries(config.inboxes)) if (inbox.id === id) return {
|
|
3288
|
-
alias,
|
|
3289
|
-
inbox
|
|
3290
|
-
};
|
|
3291
|
-
} else for (const [alias, account] of Object.entries(config.accounts)) if (account.id === id) return {
|
|
3292
|
-
alias,
|
|
3293
|
-
account
|
|
3294
|
-
};
|
|
3295
|
-
return null;
|
|
3296
|
-
}
|
|
3297
|
-
/**
|
|
3298
|
-
* The refusal for a former name, or null when `name` is not one.
|
|
3299
|
-
*
|
|
3300
|
-
* The replacement is looked up by id rather than read from the record, so a chain of renames ends at the name the
|
|
3301
|
-
* account has today — and an account removed since its rename is said to be gone, rather than pointing somebody at a
|
|
3302
|
-
* name that now belongs to nothing.
|
|
3303
|
-
*/
|
|
3304
|
-
function formerNameRefusal(config, kind, name) {
|
|
3305
|
-
if (config.version !== 2) return null;
|
|
3306
|
-
const former = own(config.formerNames[MAP[kind]], name);
|
|
3307
|
-
if (!former) return null;
|
|
3308
|
-
const current = kind === "inbox" ? findById(config, "inbox", former.id) : findById(config, "account", former.id);
|
|
3309
|
-
if (!current) return new CommsError("NOT_FOUND", `"${name}" was renamed to "${former.name}", which has since been removed`, { details: {
|
|
3310
|
-
formerName: name,
|
|
3311
|
-
id: former.id
|
|
3312
|
-
} });
|
|
3313
|
-
return new CommsError("NOT_FOUND", `"${name}" was renamed to "${current.alias}"`, {
|
|
3314
|
-
hint: `Use "${current.alias}".`,
|
|
3315
|
-
details: {
|
|
3316
|
-
formerName: name,
|
|
3317
|
-
currentName: current.alias,
|
|
3318
|
-
id: former.id
|
|
3319
|
-
}
|
|
3320
|
-
});
|
|
3321
|
-
}
|
|
3322
|
-
/**
|
|
3323
|
-
* Whether `name` may be given to a new or renamed account of this kind and platform.
|
|
3324
|
-
*
|
|
3325
|
-
* Each version keeps its own rule, because version 1's differ by kind today and must not change under a file an
|
|
3326
|
-
* older release also writes: a mailbox name need only be free among mailboxes, a workspace name among both. Version
|
|
3327
|
-
* 2 is stricter and the same for both: the grammar, the platform, free in both maps, and never a former name of
|
|
3328
|
-
* either kind.
|
|
3329
|
-
*
|
|
3330
|
-
* A check, not a reservation. Callers repeat it inside the config write, where the schema enforces the same rules
|
|
3331
|
-
* again for version 2, because anything can happen between asking and writing.
|
|
3332
|
-
*/
|
|
3333
|
-
function nameAvailable(config, kind, name, platform) {
|
|
3334
|
-
const refuse = (error) => ({
|
|
3335
|
-
ok: false,
|
|
3336
|
-
error
|
|
3337
|
-
});
|
|
3338
|
-
if (config.version === 1) {
|
|
3339
|
-
if (RESERVED_ALIASES.has(name)) return refuse(new CommsError("USAGE", `"${name}" is reserved`, { hint: "Choose another name." }));
|
|
3340
|
-
if (!isValidAlias(name)) return refuse(new CommsError("USAGE", `"${name}" is not a usable name`, { hint: "Lower-case letters, digits and dashes, up to 32 characters, not starting with a dash." }));
|
|
3341
|
-
if (kind === "inbox" ? own(config.inboxes, name) : own(config.inboxes, name) ?? own(config.accounts, name)) return refuse(new CommsError("CONFIG", `"${name}" is already connected`, { hint: "Choose another name." }));
|
|
3342
|
-
return { ok: true };
|
|
3343
|
-
}
|
|
3344
|
-
const problem = nameShapeProblem(name, platform);
|
|
3345
|
-
if (problem) return refuse(new CommsError("USAGE", problem));
|
|
3346
|
-
if (own(config.inboxes, name) || own(config.accounts, name)) return refuse(new CommsError("CONFIG", `"${name}" is already connected`, { hint: "Choose another name." }));
|
|
3347
|
-
for (const map of ["inboxes", "accounts"]) {
|
|
3348
|
-
const former = own(config.formerNames[map], name);
|
|
3349
|
-
if (former) return refuse(new CommsError("CONFIG", `"${name}" was the name of another account and cannot be used again`, {
|
|
3350
|
-
hint: "A former name keeps pointing people at the account that had it. Choose another name.",
|
|
3351
|
-
details: {
|
|
3352
|
-
formerName: name,
|
|
3353
|
-
id: former.id
|
|
3354
|
-
}
|
|
3355
|
-
}));
|
|
3356
|
-
}
|
|
3357
|
-
return { ok: true };
|
|
3358
|
-
}
|
|
3359
|
-
function okEnvelope(data) {
|
|
3360
|
-
return {
|
|
3361
|
-
ok: true,
|
|
3362
|
-
schemaVersion: 1,
|
|
3363
|
-
data
|
|
3364
|
-
};
|
|
3365
|
-
}
|
|
3366
|
-
function errorEnvelope(error) {
|
|
3367
|
-
const body = {
|
|
3368
|
-
code: error.code,
|
|
3369
|
-
message: error.message
|
|
3370
|
-
};
|
|
3371
|
-
if (error.hint !== void 0) body.hint = error.hint;
|
|
3372
|
-
if (error.details !== void 0) body.details = error.details;
|
|
3373
|
-
return {
|
|
3374
|
-
ok: false,
|
|
3375
|
-
schemaVersion: 1,
|
|
3376
|
-
error: body
|
|
3377
|
-
};
|
|
3378
|
-
}
|
|
3379
|
-
const defaultStreams = {
|
|
3380
|
-
stdout: process.stdout,
|
|
3381
|
-
stderr: process.stderr,
|
|
3382
|
-
stdin: process.stdin
|
|
3383
|
-
};
|
|
3384
|
-
function colorEnabled(env, stream, flag) {
|
|
3385
|
-
if (flag === false) return false;
|
|
3386
|
-
if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return false;
|
|
3387
|
-
if (env.TERM === "dumb") return false;
|
|
3388
|
-
if (env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "0") return true;
|
|
3389
|
-
return Boolean(stream.isTTY);
|
|
3390
|
-
}
|
|
3391
|
-
/** True when a human could answer a prompt: both ends are terminals, no --json/--no-input, not CI. */
|
|
3392
|
-
function canPrompt(env, streams, options) {
|
|
3393
|
-
if (options.json || options.noInput) return false;
|
|
3394
|
-
if (env.CI && env.CI !== "0" && env.CI !== "false") return false;
|
|
3395
|
-
return Boolean(streams.stdin?.isTTY && streams.stdout.isTTY);
|
|
3396
|
-
}
|
|
3397
|
-
/** Environment variables well-known coding agents set. Used only as a speed bump, never as a security boundary. */
|
|
3398
|
-
const AGENT_MARKERS = [
|
|
3399
|
-
"CLAUDECODE",
|
|
3400
|
-
"CLAUDE_CODE_ENTRYPOINT",
|
|
3401
|
-
"CODEX_SANDBOX",
|
|
3402
|
-
"CODEX_HOME",
|
|
3403
|
-
"CURSOR_AGENT",
|
|
3404
|
-
"GEMINI_CLI",
|
|
3405
|
-
"AGENT_COMMS_AGENT"
|
|
3406
|
-
];
|
|
3407
|
-
function agentMarker(env) {
|
|
3408
|
-
for (const name of AGENT_MARKERS) if (env[name] !== void 0 && env[name] !== "") return name;
|
|
3409
|
-
return null;
|
|
3410
|
-
}
|
|
3411
|
-
function paint(color, format, text) {
|
|
3412
|
-
return color ? styleText(format, text, { validateStream: false }) : text;
|
|
3413
|
-
}
|
|
3414
|
-
/**
|
|
3415
|
-
* A command line for a person to copy and run, each word quoted only where the shell it is pasted into would need it.
|
|
3416
|
-
* Every command this package prints to be run — a change to run again with its approval, a folder to take out, an
|
|
3417
|
-
* entry to register again or remove — is quoted here, so no printer quotes for a shell of its own.
|
|
3418
|
-
*
|
|
3419
|
-
* Everywhere but Windows that shell is a POSIX one, and a word goes in single quotes, inside which nothing is special
|
|
3420
|
-
* but the quote itself. Windows has two shells, and neither reads single quotes that way: cmd.exe does not take them
|
|
3421
|
-
* as quotes at all, so `'C:\Profiles\First Last\outgoing'` reached the command as two words, quote marks and all, and
|
|
3422
|
-
* PowerShell does, but escapes a quote inside them by doubling it rather than as `'\''`. A command printed on Windows
|
|
3423
|
-
* has to be safe in both, because nothing says which one it will be pasted into (CUE-306) — and safe is not enough:
|
|
3424
|
-
* the program has to receive the same words from either. Three readers stand between the line and the program. cmd.exe
|
|
3425
|
-
* hands the program the line as it is, and the program's own parser (the C runtime's, Node's) splits it. PowerShell
|
|
3426
|
-
* reads the line itself, a double-quoted word with its backslashes as plain characters, and then writes a new command
|
|
3427
|
-
* line for the program: Windows PowerShell 5.1 the old way ("Legacy" in about_Parsing, "Passing arguments to native
|
|
3428
|
-
* applications") — a word quoted only when it holds whitespace, as it is, and an empty word dropped — and PowerShell
|
|
3429
|
-
* 7.3 and later its own way ("Standard") — quoted when it must be, with every backslash before a quote doubled. For a
|
|
3430
|
-
* `.cmd` script, which is how npm installs `agentcomms`, `claude` and `codex` on Windows, PowerShell 7 goes back to the
|
|
3431
|
-
* old way, and cmd.exe then reads that new line. So on Windows:
|
|
3432
|
-
*
|
|
3433
|
-
* - A word of letters, digits and `_ + = : . / \`, with `@` and `-` anywhere but first, is left as it is: no reader
|
|
3434
|
-
* does anything with it, and a backslash is an ordinary character to all of them, at the end too. A first `@` is
|
|
3435
|
-
* splatting to PowerShell, and a `,` its array operator — two words — so a word with either is quoted. So is a word
|
|
3436
|
-
* that starts with a digit, which PowerShell may read as a number (`1kb`, `0x10`), and one that starts with `-` but
|
|
3437
|
-
* is not a plain option, which PowerShell may read as a parameter of its own and split (`-name.x`).
|
|
3438
|
-
* - A word that double quotes bring through all three readers whole goes in double quotes. Inside them cmd.exe reads
|
|
3439
|
-
* `& | < > ^ ( )` and spaces as ordinary characters, and PowerShell reads everything as ordinary but `$`, the
|
|
3440
|
-
* backtick and a double quote. What is left special in one or the other is kept out: a double quote, which ends the
|
|
3441
|
-
* quoting in both — and PowerShell takes the curly ones, `“ ” „`, for one too; `$` and the backtick, PowerShell's
|
|
3442
|
-
* expansion and escape; `%`, which cmd.exe expands as `%NAME%` before it looks at quotes at all; `!`, which it
|
|
3443
|
-
* expands as `!NAME!` inside quotes too wherever delayed expansion is on, and which then makes a `^` inside quotes an
|
|
3444
|
-
* escape; and any control or formatting character — a line break ends the command in cmd.exe even inside quotes, a
|
|
3445
|
-
* tab pasted into cmd.exe can complete a file name, and a right-to-left override shows a line other than the one
|
|
3446
|
-
* that runs. Three more cannot come through: an empty word, which Windows PowerShell drops; a word ending in a
|
|
3447
|
-
* backslash, which the C runtime reads with the closing quote as `\"` — and doubled for it, PowerShell 7 passes both
|
|
3448
|
-
* backslashes on where cmd.exe and Windows PowerShell pass one; and a word with `& | < > ^ ( )` but no whitespace,
|
|
3449
|
-
* which PowerShell passes to a `.cmd` script unquoted, for cmd.exe to run `&whoami` from or to split at `|`.
|
|
3450
|
-
* - With any other word in it, no line is printed at all (`line` is null), and the printers show the command's words
|
|
3451
|
-
* as JSON instead, saying it has to be typed. Not a line with the word left out: a placeholder in its place was
|
|
3452
|
-
* still a command that ran — `claude mcp remove NAME` removed whatever entry was called `NAME`, and an install
|
|
3453
|
-
* hint's `--force` replaced it — and before that, a word quoted for PowerShell's single quotes, which cmd.exe reads
|
|
3454
|
-
* as characters, ran `whoami` from a server named `$x&whoami&`. See `commandAsJson` for why the JSON runs nothing.
|
|
3455
|
-
*
|
|
3456
|
-
* Everywhere else there is always a line: single quotes make any word safe.
|
|
3457
|
-
*/
|
|
3458
|
-
function shellCommand(words, platform = process.platform) {
|
|
3459
|
-
if (platform !== "win32") return {
|
|
3460
|
-
words,
|
|
3461
|
-
line: words.map(posixShellWord).join(" "),
|
|
3462
|
-
platform
|
|
3463
|
-
};
|
|
3464
|
-
const printed = words.map(windowsShellWord);
|
|
3465
|
-
return {
|
|
3466
|
-
words,
|
|
3467
|
-
line: printed.includes(null) ? null : printed.join(" "),
|
|
3468
|
-
platform
|
|
3469
|
-
};
|
|
3470
|
-
}
|
|
3471
|
-
/** The same command with more words at its end — the approval it is to be run again with — for the same shell. */
|
|
3472
|
-
function withWords(command, ...more) {
|
|
3473
|
-
return shellCommand([...command.words, ...more], command.platform);
|
|
3474
|
-
}
|
|
3475
|
-
function posixShellWord(word) {
|
|
3476
|
-
return /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`;
|
|
3477
|
-
}
|
|
3478
|
-
/**
|
|
3479
|
-
* The word as cmd.exe, Windows PowerShell and PowerShell 7 all hand it to a program, or null when no printing of it
|
|
3480
|
-
* does (see `shellCommand`). Bare: a plain option, or ordinary characters that do not start like a number.
|
|
3481
|
-
*/
|
|
3482
|
-
function windowsShellWord(word) {
|
|
3483
|
-
if (/^(?:--?[A-Za-z][A-Za-z0-9-]*|(?!\+?\.?\d)[\w+=:./\\][\w@+=:./\\-]*)$/.test(word)) return word;
|
|
3484
|
-
if (word === "" || word.endsWith("\\")) return null;
|
|
3485
|
-
if (/["$`%!\u201C-\u201E]|[\p{C}\p{Zl}\p{Zp}]/u.test(word)) return null;
|
|
3486
|
-
if (/[&|<>^()]/.test(word) && !/\s/.test(word)) return null;
|
|
3487
|
-
return `"${word}"`;
|
|
3488
|
-
}
|
|
3489
|
-
/**
|
|
3490
|
-
* The words of a command as a JSON array, for a command that cannot be printed as a line: something to read, and to
|
|
3491
|
-
* parse, and nothing to run.
|
|
3492
|
-
*
|
|
3493
|
-
* Pasted into either Windows shell by mistake, it runs nothing. PowerShell refuses it before running anything: a `[`
|
|
3494
|
-
* opens a type name, and a quoted string is none. cmd.exe looks for a program called `["agentcomms"` or the like, and
|
|
3495
|
-
* finds none — and nothing in the line is anything else to it: every character it acts on, inside quotes or out (`%`,
|
|
3496
|
-
* `!`), is written as a `\u` escape, and so is every double quote inside a word, so the quotes cmd.exe sees are the
|
|
3497
|
-
* JSON's own, in pairs, and `&`, `|`, `<`, `>`, `^` and the parentheses are only ever inside them, where it reads them
|
|
3498
|
-
* as characters. `$` and the backtick are escaped too, so that PowerShell would expand nothing even if it read on, and
|
|
3499
|
-
* so is everything outside printable ASCII — a line break, a curly quote, a right-to-left override. A backslash is
|
|
3500
|
-
* doubled, as JSON has it.
|
|
3501
|
-
*/
|
|
3502
|
-
function commandAsJson(words) {
|
|
3503
|
-
const word = (text) => {
|
|
3504
|
-
let json = "";
|
|
3505
|
-
for (let index = 0; index < text.length; index++) {
|
|
3506
|
-
const code = text.charCodeAt(index);
|
|
3507
|
-
const char = text[index];
|
|
3508
|
-
if (char === "\\") json += "\\\\";
|
|
3509
|
-
else if (code >= 32 && code <= 126 && !"\"%!$`".includes(char)) json += char;
|
|
3510
|
-
else json += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
3511
|
-
}
|
|
3512
|
-
return `"${json}"`;
|
|
3513
|
-
};
|
|
3514
|
-
return `[${words.map(word).join(",")}]`;
|
|
3515
|
-
}
|
|
3516
|
-
/** What is said beside a command shown as JSON. */
|
|
3517
|
-
const TO_TYPE = "the command's words, written as JSON: one of them cannot be quoted the same way for cmd.exe and for PowerShell, so type the command yourself, with that word quoted for the shell you use";
|
|
3518
|
-
/** A command in backticks, for a sentence — as its words in JSON, saying it has to be typed, when it has no line. */
|
|
3519
|
-
function inlineCommand(command) {
|
|
3520
|
-
return command.line === null ? `\`${commandAsJson(command.words)}\` (${TO_TYPE})` : `\`${command.line}\``;
|
|
3521
|
-
}
|
|
3522
|
-
/** A command as text of its own — a list's line, a field's value — or its words in JSON, saying it has to be typed. */
|
|
3523
|
-
function commandText(command) {
|
|
3524
|
-
return command.line === null ? `${commandAsJson(command.words)} (${TO_TYPE})` : command.line;
|
|
3474
|
+
* of an inbox called `constructor`, which version 1 allows, found it.
|
|
3475
|
+
*/
|
|
3476
|
+
function own(map, key) {
|
|
3477
|
+
return Object.hasOwn(map, key) ? map[key] : void 0;
|
|
3525
3478
|
}
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3479
|
+
function resolveName(config, kind, name, notFound) {
|
|
3480
|
+
if (kind === "inbox") {
|
|
3481
|
+
const inbox = own(config.inboxes, name);
|
|
3482
|
+
if (inbox) return {
|
|
3483
|
+
alias: name,
|
|
3484
|
+
inbox
|
|
3485
|
+
};
|
|
3486
|
+
} else {
|
|
3487
|
+
const account = own(config.accounts, name);
|
|
3488
|
+
if (account) return {
|
|
3489
|
+
alias: name,
|
|
3490
|
+
account
|
|
3491
|
+
};
|
|
3531
3492
|
}
|
|
3532
|
-
const
|
|
3533
|
-
if (
|
|
3493
|
+
const renamed = formerNameRefusal(config, kind, name);
|
|
3494
|
+
if (renamed) throw renamed;
|
|
3495
|
+
throw notFound?.() ?? defaultNotFound(config, kind, name);
|
|
3534
3496
|
}
|
|
3535
|
-
/**
|
|
3536
|
-
function
|
|
3537
|
-
const
|
|
3538
|
-
|
|
3539
|
-
else {
|
|
3540
|
-
streams.stderr.write(`${paint(options.color, "red", "error")}: ${commsError.message}\n`);
|
|
3541
|
-
if (commsError.hint) streams.stderr.write(`${paint(options.color, "dim", "hint")}: ${commsError.hint}\n`);
|
|
3542
|
-
}
|
|
3543
|
-
return commsError.exitCode;
|
|
3497
|
+
/** What to say when there are no mailboxes at all: the command that connects one, from the mail channel's manifest. */
|
|
3498
|
+
function noInboxesHint() {
|
|
3499
|
+
const connect = connectMailboxCommand();
|
|
3500
|
+
return connect ? `No inboxes yet: add one with \`${connect}\`.` : "No inboxes yet.";
|
|
3544
3501
|
}
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
return EXIT_CODES.OK;
|
|
3550
|
-
} catch (error) {
|
|
3551
|
-
return writeError(error, options, streams);
|
|
3502
|
+
function defaultNotFound(config, kind, name) {
|
|
3503
|
+
if (kind === "inbox") {
|
|
3504
|
+
const known = Object.keys(config.inboxes);
|
|
3505
|
+
return new CommsError("NOT_FOUND", `no inbox called "${name}"`, { hint: known.length ? `Known inboxes: ${known.join(", ")}.` : noInboxesHint() });
|
|
3552
3506
|
}
|
|
3507
|
+
return new CommsError("NOT_FOUND", `no account called "${name}"`);
|
|
3508
|
+
}
|
|
3509
|
+
function lookupName(config, kind, name) {
|
|
3510
|
+
return kind === "inbox" ? own(config.inboxes, name) : own(config.accounts, name);
|
|
3511
|
+
}
|
|
3512
|
+
function findById(config, kind, id) {
|
|
3513
|
+
if (kind === "inbox") {
|
|
3514
|
+
for (const [alias, inbox] of Object.entries(config.inboxes)) if (inbox.id === id) return {
|
|
3515
|
+
alias,
|
|
3516
|
+
inbox
|
|
3517
|
+
};
|
|
3518
|
+
} else for (const [alias, account] of Object.entries(config.accounts)) if (account.id === id) return {
|
|
3519
|
+
alias,
|
|
3520
|
+
account
|
|
3521
|
+
};
|
|
3522
|
+
return null;
|
|
3553
3523
|
}
|
|
3554
3524
|
/**
|
|
3555
|
-
* The
|
|
3556
|
-
* whose code is not its own to make up.
|
|
3525
|
+
* The refusal for a former name, or null when `name` is not one.
|
|
3557
3526
|
*
|
|
3558
|
-
*
|
|
3559
|
-
*
|
|
3527
|
+
* The replacement is looked up by id rather than read from the record, so a chain of renames ends at the name the
|
|
3528
|
+
* account has today — and an account removed since its rename is said to be gone, rather than pointing somebody at a
|
|
3529
|
+
* name that now belongs to nothing.
|
|
3560
3530
|
*/
|
|
3561
|
-
function
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3531
|
+
function formerNameRefusal(config, kind, name) {
|
|
3532
|
+
if (config.version !== 2) return null;
|
|
3533
|
+
const former = own(config.formerNames[MAP[kind]], name);
|
|
3534
|
+
if (!former) return null;
|
|
3535
|
+
const current = kind === "inbox" ? findById(config, "inbox", former.id) : findById(config, "account", former.id);
|
|
3536
|
+
if (!current) return new CommsError("NOT_FOUND", `"${name}" was renamed to "${former.name}", which has since been removed`, { details: {
|
|
3537
|
+
formerName: name,
|
|
3538
|
+
id: former.id
|
|
3539
|
+
} });
|
|
3540
|
+
return new CommsError("NOT_FOUND", `"${name}" was renamed to "${current.alias}"`, {
|
|
3541
|
+
hint: `Use "${current.alias}".`,
|
|
3542
|
+
details: {
|
|
3543
|
+
formerName: name,
|
|
3544
|
+
currentName: current.alias,
|
|
3545
|
+
id: former.id
|
|
3546
|
+
}
|
|
3566
3547
|
});
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3548
|
+
}
|
|
3549
|
+
/**
|
|
3550
|
+
* Whether `name` may be given to a new or renamed account of this kind and platform.
|
|
3551
|
+
*
|
|
3552
|
+
* Each version keeps its own rule, because version 1's differ by kind today and must not change under a file an
|
|
3553
|
+
* older release also writes: a mailbox name need only be free among mailboxes, a workspace name among both. Version
|
|
3554
|
+
* 2 is stricter and the same for both: the grammar, the platform, free in both maps, and never a former name of
|
|
3555
|
+
* either kind.
|
|
3556
|
+
*
|
|
3557
|
+
* A check, not a reservation. Callers repeat it inside the config write, where the schema enforces the same rules
|
|
3558
|
+
* again for version 2, because anything can happen between asking and writing.
|
|
3559
|
+
*/
|
|
3560
|
+
function nameAvailable(config, kind, name, platform) {
|
|
3561
|
+
const refuse = (error) => ({
|
|
3562
|
+
ok: false,
|
|
3563
|
+
error
|
|
3564
|
+
});
|
|
3565
|
+
if (config.version === 1) {
|
|
3566
|
+
if (RESERVED_ALIASES.has(name)) return refuse(new CommsError("USAGE", `"${name}" is reserved`, { hint: "Choose another name." }));
|
|
3567
|
+
if (!isValidAlias(name)) return refuse(new CommsError("USAGE", `"${name}" is not a usable name`, { hint: "Lower-case letters, digits and dashes, up to 32 characters, not starting with a dash." }));
|
|
3568
|
+
if (kind === "inbox" ? own(config.inboxes, name) : own(config.inboxes, name) ?? own(config.accounts, name)) return refuse(new CommsError("CONFIG", `"${name}" is already connected`, { hint: "Choose another name." }));
|
|
3569
|
+
return { ok: true };
|
|
3570
|
+
}
|
|
3571
|
+
const problem = nameShapeProblem(name, platform);
|
|
3572
|
+
if (problem) return refuse(new CommsError("USAGE", problem));
|
|
3573
|
+
if (own(config.inboxes, name) || own(config.accounts, name)) return refuse(new CommsError("CONFIG", `"${name}" is already connected`, { hint: "Choose another name." }));
|
|
3574
|
+
for (const map of ["inboxes", "accounts"]) {
|
|
3575
|
+
const former = own(config.formerNames[map], name);
|
|
3576
|
+
if (former) return refuse(new CommsError("CONFIG", `"${name}" was the name of another account and cannot be used again`, {
|
|
3577
|
+
hint: "A former name keeps pointing people at the account that had it. Choose another name.",
|
|
3578
|
+
details: {
|
|
3579
|
+
formerName: name,
|
|
3580
|
+
id: former.id
|
|
3581
|
+
}
|
|
3582
|
+
}));
|
|
3583
|
+
}
|
|
3584
|
+
return { ok: true };
|
|
3571
3585
|
}
|
|
3572
3586
|
/**
|
|
3573
3587
|
* Enough of TOML to read codex's `config.toml`, and a refusal for anything else.
|
|
@@ -4995,7 +5009,7 @@ async function mcpInstall(context, product, options, planned) {
|
|
|
4995
5009
|
};
|
|
4996
5010
|
const existing = scan.servers;
|
|
4997
5011
|
const { cliName, binary, own, configPath, writes } = target;
|
|
4998
|
-
const warnings = (product.warnAbout?.(existing.filter((server) => server.client === options.client)) ?? []).filter(Boolean);
|
|
5012
|
+
const warnings = (product.warnAbout?.(existing.filter((server) => server.client === options.client), context.platform) ?? []).filter(Boolean);
|
|
4999
5013
|
if (kept.length > 0) {
|
|
5000
5014
|
const removal = options.client === "claude-code" ? inlineCommand(shellCommand([
|
|
5001
5015
|
"claude",
|
|
@@ -5501,8 +5515,8 @@ function findRivalPackageServers(servers, packages, platform = process.platform)
|
|
|
5501
5515
|
return findings;
|
|
5502
5516
|
}
|
|
5503
5517
|
/** What registering a server says about the rival packages its manifest names: one line each. */
|
|
5504
|
-
function rivalPackageWarnings(servers, packages) {
|
|
5505
|
-
return findRivalPackageServers(servers, packages).map((finding) => `${finding.packageName} is registered with ${finding.client} as "${finding.name}": ${finding.reason}. Remove it: ${finding.removal}`);
|
|
5518
|
+
function rivalPackageWarnings(servers, packages, platform = process.platform) {
|
|
5519
|
+
return findRivalPackageServers(servers, packages, platform).map((finding) => `${finding.packageName} is registered with ${finding.client} as "${finding.name}": ${finding.reason}. Remove it: ${finding.removal}`);
|
|
5506
5520
|
}
|
|
5507
5521
|
/**
|
|
5508
5522
|
* Registered servers that name the service's `word` anywhere — their name, command, arguments or URL — and are not
|
|
@@ -5588,7 +5602,7 @@ function serverFactsOf({ packageName, manifest }) {
|
|
|
5588
5602
|
if (rivals === void 0) return facts;
|
|
5589
5603
|
return {
|
|
5590
5604
|
...facts,
|
|
5591
|
-
warnAbout: (servers) => [...rivals.packages ? rivalPackageWarnings(servers, rivals.packages) : [], ...rivals.word !== void 0 && rivals.can !== void 0 ? rivalWordWarnings(servers, rivals.word, rivals.can, facts) : []]
|
|
5605
|
+
warnAbout: (servers, platform) => [...rivals.packages ? rivalPackageWarnings(servers, rivals.packages, platform) : [], ...rivals.word !== void 0 && rivals.can !== void 0 ? rivalWordWarnings(servers, rivals.word, rivals.can, facts) : []]
|
|
5592
5606
|
};
|
|
5593
5607
|
}
|
|
5594
5608
|
const CHANNEL_SERVERS = Object.freeze(Object.fromEntries(CHANNEL_SNAPSHOT.map((entry) => [entry.manifest.channel, serverFactsOf(entry)])));
|
|
@@ -5786,9 +5800,9 @@ function renderUpdate(result, color) {
|
|
|
5786
5800
|
if (result.next) lines.push("", result.next);
|
|
5787
5801
|
return lines.join("\n");
|
|
5788
5802
|
}
|
|
5789
|
-
/** The
|
|
5790
|
-
function
|
|
5791
|
-
return
|
|
5803
|
+
/** The channel's fixed approval-command prefix with this generated id, rendered by the common shell rule. */
|
|
5804
|
+
function changeApprovalCommand(approveCommand, approvalId, platform = process.platform) {
|
|
5805
|
+
return shellCommand([...(approveCommand ?? "agentcomms approve").trim().split(/\s+/), approvalId], platform);
|
|
5792
5806
|
}
|
|
5793
5807
|
/**
|
|
5794
5808
|
* The change policy that governs a change, as `config` stands: the strictest of the policies over everything the change
|
|
@@ -5899,7 +5913,7 @@ async function prepareChange(core, request, options) {
|
|
|
5899
5913
|
change: binding
|
|
5900
5914
|
}),
|
|
5901
5915
|
expiresAt: record.expiresAt,
|
|
5902
|
-
next: nextStep(record.approvalId, policy,
|
|
5916
|
+
next: nextStep(record.approvalId, policy, options)
|
|
5903
5917
|
};
|
|
5904
5918
|
} catch (error) {
|
|
5905
5919
|
await auditRefusal(core, "change.prepare", error, {
|
|
@@ -5930,7 +5944,10 @@ async function claimChange(core, approvalId, expect, options) {
|
|
|
5930
5944
|
const record = await core.approvals.claimForChange(approvalId, {
|
|
5931
5945
|
change: binding,
|
|
5932
5946
|
policy
|
|
5933
|
-
}, {
|
|
5947
|
+
}, {
|
|
5948
|
+
pendingHint: `Ask the user to run ${inlineCommand(changeApprovalCommand(options.approveCommand, approvalId, options.platform))} in their own terminal, then try again with the same approval.`,
|
|
5949
|
+
platform: options.platform
|
|
5950
|
+
});
|
|
5934
5951
|
const decided = stricterPolicy(policy, record.requiredPolicy) === "chat" ? "chat" : "confirm";
|
|
5935
5952
|
await auditChange(core, {
|
|
5936
5953
|
operation: "change.claim",
|
|
@@ -5978,7 +5995,7 @@ async function beginChangeApproval(core, approvalId, options) {
|
|
|
5978
5995
|
let record;
|
|
5979
5996
|
try {
|
|
5980
5997
|
record = await changeRecord(core, approvalId);
|
|
5981
|
-
const challenge = await core.approvals.issueChallenge(approvalId, "change");
|
|
5998
|
+
const challenge = await core.approvals.issueChallenge(approvalId, "change", options.platform);
|
|
5982
5999
|
return {
|
|
5983
6000
|
approvalId,
|
|
5984
6001
|
preview: renderChangePreview(record),
|
|
@@ -6003,7 +6020,7 @@ async function finishChangeApproval(core, approvalId, answer, options) {
|
|
|
6003
6020
|
const approved = await core.approvals.approve(approvalId, "terminal", {
|
|
6004
6021
|
draftMessageId: digest,
|
|
6005
6022
|
digest
|
|
6006
|
-
}, answer, "change");
|
|
6023
|
+
}, answer, "change", options.platform);
|
|
6007
6024
|
await auditChange(core, {
|
|
6008
6025
|
operation: "change.approve",
|
|
6009
6026
|
outcome: "ok",
|
|
@@ -6059,8 +6076,8 @@ async function revokeChange(core, approvalId, reason, options) {
|
|
|
6059
6076
|
function changePolicyOf$1(record) {
|
|
6060
6077
|
return record.requiredPolicy === "chat" ? "chat" : "confirm";
|
|
6061
6078
|
}
|
|
6062
|
-
function nextStep(approvalId, policy,
|
|
6063
|
-
return policy === "chat" ? `Show this preview to the user and ask. If they say yes, claim approval ${approvalId} and apply the change; if not, revoke it.` : `The change policy is confirm: ask the user to run
|
|
6079
|
+
function nextStep(approvalId, policy, options) {
|
|
6080
|
+
return policy === "chat" ? `Show this preview to the user and ask. If they say yes, claim approval ${approvalId} and apply the change; if not, revoke it.` : `The change policy is confirm: ask the user to run ${inlineCommand(changeApprovalCommand(options.approveCommand, approvalId, options.platform))} in their own terminal and type the code it shows. Then claim approval ${approvalId} and apply the change.`;
|
|
6064
6081
|
}
|
|
6065
6082
|
/**
|
|
6066
6083
|
* One line per step of a change approval: who asked, from which surface, under which policy, and how it ended.
|
|
@@ -6209,12 +6226,14 @@ async function gatedChange(core, change, options) {
|
|
|
6209
6226
|
status: "approval-required",
|
|
6210
6227
|
prepared: await prepareChange(core, request, {
|
|
6211
6228
|
surface: options.surface,
|
|
6212
|
-
approveCommand: options.approveCommand
|
|
6229
|
+
approveCommand: options.approveCommand,
|
|
6230
|
+
platform: options.platform
|
|
6213
6231
|
})
|
|
6214
6232
|
};
|
|
6215
6233
|
const consent = await claimChange(core, options.approvalId, request, {
|
|
6216
6234
|
surface: options.surface,
|
|
6217
|
-
approveCommand: options.approveCommand
|
|
6235
|
+
approveCommand: options.approveCommand,
|
|
6236
|
+
platform: options.platform
|
|
6218
6237
|
});
|
|
6219
6238
|
return {
|
|
6220
6239
|
status: "applied",
|
|
@@ -6276,7 +6295,8 @@ async function gatedChangeAtTerminal(core, change, options) {
|
|
|
6276
6295
|
const first = await gatedChange(core, change, {
|
|
6277
6296
|
surface: "cli",
|
|
6278
6297
|
approvalId: options.approvalId,
|
|
6279
|
-
approveCommand
|
|
6298
|
+
approveCommand,
|
|
6299
|
+
platform: options.output.platform
|
|
6280
6300
|
});
|
|
6281
6301
|
if (first.status === "applied") return first.result;
|
|
6282
6302
|
const { prepared } = first;
|
|
@@ -6285,7 +6305,7 @@ async function gatedChangeAtTerminal(core, change, options) {
|
|
|
6285
6305
|
const carrying = [options.approvalFlag ?? "--approval", prepared.approvalId];
|
|
6286
6306
|
const { command } = options;
|
|
6287
6307
|
throw new CommsError("APPROVAL_PENDING", `this change needs approval first: ${prepared.summary}`, {
|
|
6288
|
-
hint: approvalHint(prepared, typeof command === "string" ? `${command} ${carrying.join(" ")}` : withWords(command, ...carrying), approveCommand),
|
|
6308
|
+
hint: approvalHint(prepared, typeof command === "string" ? `${command} ${carrying.join(" ")}` : withWords(command, ...carrying), approveCommand, options.output.platform),
|
|
6289
6309
|
details: {
|
|
6290
6310
|
approvalId: prepared.approvalId,
|
|
6291
6311
|
policy: prepared.policy,
|
|
@@ -6306,7 +6326,8 @@ async function gatedChangeAtTerminal(core, change, options) {
|
|
|
6306
6326
|
const second = await gatedChange(core, change, {
|
|
6307
6327
|
surface: "cli",
|
|
6308
6328
|
approvalId: prepared.approvalId,
|
|
6309
|
-
approveCommand
|
|
6329
|
+
approveCommand,
|
|
6330
|
+
platform: options.output.platform
|
|
6310
6331
|
});
|
|
6311
6332
|
if (second.status !== "applied") throw new CommsError("UNEXPECTED", "the approved change asked for approval again");
|
|
6312
6333
|
return second.result;
|
|
@@ -6318,9 +6339,9 @@ async function gatedChangeAtTerminal(core, change, options) {
|
|
|
6318
6339
|
* Shared with a command that reports a waiting change rather than failing on it — `agent-gmail setup`, which stops at
|
|
6319
6340
|
* its registration step and says why in its report — so the two say it in the same words.
|
|
6320
6341
|
*/
|
|
6321
|
-
function approvalHint(prepared, rerun, approveCommand) {
|
|
6342
|
+
function approvalHint(prepared, rerun, approveCommand, platform = typeof rerun === "string" ? process.platform : rerun.platform) {
|
|
6322
6343
|
const run = typeof rerun === "string" ? `\`${rerun}\`` : inlineCommand(rerun);
|
|
6323
|
-
return prepared.policy === "confirm" ? `Show the person the preview. They run
|
|
6344
|
+
return prepared.policy === "confirm" ? `Show the person the preview. They run ${inlineCommand(changeApprovalCommand(approveCommand, prepared.approvalId, platform))}; then run ${run}.` : `Show the person the preview. Once they say yes, run ${run}.`;
|
|
6324
6345
|
}
|
|
6325
6346
|
/**
|
|
6326
6347
|
* Approves a configuration change at a terminal, the way a person approves a send: read the preview, type the code.
|
|
@@ -6340,7 +6361,11 @@ async function approveChangeAtTerminal(core, approvalId, env, output, streams =
|
|
|
6340
6361
|
refuseUnlessPerson(env, streams, {
|
|
6341
6362
|
refusedToAgent: "only a person can approve a change, not an agent",
|
|
6342
6363
|
refusedWithoutTerminal: "approving a change needs an interactive terminal",
|
|
6343
|
-
command:
|
|
6364
|
+
command: commandText(shellCommand([
|
|
6365
|
+
"agentcomms",
|
|
6366
|
+
"approve",
|
|
6367
|
+
approvalId
|
|
6368
|
+
], output.platform)),
|
|
6344
6369
|
color: output.color,
|
|
6345
6370
|
json: output.json
|
|
6346
6371
|
});
|
|
@@ -7002,7 +7027,7 @@ async function npmInstallGlobal(env, spec) {
|
|
|
7002
7027
|
}
|
|
7003
7028
|
}
|
|
7004
7029
|
/** The package version, read from package.json at build time. */
|
|
7005
|
-
const VERSION = "0.12.
|
|
7030
|
+
const VERSION = "0.12.3";
|
|
7006
7031
|
/**
|
|
7007
7032
|
* Registering, pruning and listing the MCP servers of every channel, from the core.
|
|
7008
7033
|
*
|
|
@@ -7260,7 +7285,7 @@ function serverPruneChange(core, env, request, own) {
|
|
|
7260
7285
|
};
|
|
7261
7286
|
const { packageName, version } = ownProduct(request.channel, own) ?? {
|
|
7262
7287
|
packageName: channelServer(request.channel).packageName,
|
|
7263
|
-
version: "0.12.
|
|
7288
|
+
version: "0.12.3"
|
|
7264
7289
|
};
|
|
7265
7290
|
const product = {
|
|
7266
7291
|
packageName,
|
|
@@ -8937,7 +8962,7 @@ function terminalUpdateHooks(core, env, options) {
|
|
|
8937
8962
|
};
|
|
8938
8963
|
}
|
|
8939
8964
|
//#endregion
|
|
8940
|
-
//#region ../core/dist/tool-arguments-
|
|
8965
|
+
//#region ../core/dist/tool-arguments-BMgRZ6DZ.mjs
|
|
8941
8966
|
const APPLIED = Symbol.for("agentcomms.strictToolArguments");
|
|
8942
8967
|
const TARGET = "draft-2020-12";
|
|
8943
8968
|
/**
|
|
@@ -20142,7 +20167,7 @@ function keyPermissionOf(account) {
|
|
|
20142
20167
|
* wrong. Core's schema reads any mode word so that one odd account cannot make the whole file unreadable; acting on
|
|
20143
20168
|
* one is refused here, as Slack refuses its own.
|
|
20144
20169
|
*/
|
|
20145
|
-
function checkedAccount(name, account) {
|
|
20170
|
+
function checkedAccount(name, account, platform = process.platform) {
|
|
20146
20171
|
const problem = (() => {
|
|
20147
20172
|
if (account.platform !== "resend") return `it is a ${account.platform} account`;
|
|
20148
20173
|
if (!MODES.includes(String(account.mode))) return `its mode is "${String(account.mode)}", and a Resend account is in read or send mode`;
|
|
@@ -20152,57 +20177,67 @@ function checkedAccount(name, account) {
|
|
|
20152
20177
|
if (lock !== void 0 && (typeof lock !== "string" || !DOMAIN.test(lock))) return "its domainLock is not a domain";
|
|
20153
20178
|
return null;
|
|
20154
20179
|
})();
|
|
20155
|
-
if (problem !== null) throw new CommsError("CONFIG", `"${name}" is not a Resend account this release can act on: ${problem}`, { hint: `Remove it with
|
|
20180
|
+
if (problem !== null) throw new CommsError("CONFIG", `"${name}" is not a Resend account this release can act on: ${problem}`, { hint: `Remove it with ${inlineCommand(shellCommand([
|
|
20181
|
+
"agent-resend",
|
|
20182
|
+
"account",
|
|
20183
|
+
"remove",
|
|
20184
|
+
name
|
|
20185
|
+
], platform))} and add it again, or fix it in the configuration file.` });
|
|
20156
20186
|
return account;
|
|
20157
20187
|
}
|
|
20158
20188
|
function notFound(name) {
|
|
20159
20189
|
return () => new CommsError("NOT_FOUND", `there is no Resend account called "${name}"`, { hint: "List them with `agent-resend account list`; a person adds one with `agent-resend account add <org/resend>`." });
|
|
20160
20190
|
}
|
|
20161
20191
|
/** Every Resend account in `config`, checked, by name. */
|
|
20162
|
-
function resendAccounts(config) {
|
|
20192
|
+
function resendAccounts(config, platform = process.platform) {
|
|
20163
20193
|
return Object.entries(config.accounts).filter(([, account]) => account.platform === PLATFORM).map(([name, account]) => ({
|
|
20164
20194
|
name,
|
|
20165
|
-
account: checkedAccount(name, account)
|
|
20195
|
+
account: checkedAccount(name, account, platform)
|
|
20166
20196
|
})).sort((a, b) => a.name < b.name ? -1 : 1);
|
|
20167
20197
|
}
|
|
20168
20198
|
/**
|
|
20169
20199
|
* The account by name, or a refusal that says what to do — through core's `resolveName`, so a name that was replaced
|
|
20170
20200
|
* is refused with the one it has now rather than reported as unknown.
|
|
20171
20201
|
*/
|
|
20172
|
-
function requireAccount(config, name) {
|
|
20202
|
+
function requireAccount(config, name, platform = process.platform) {
|
|
20173
20203
|
const { alias, account } = resolveName(config, "account", name, notFound(name));
|
|
20174
20204
|
if (account.platform !== "resend") throw notFound(name)();
|
|
20175
20205
|
return {
|
|
20176
20206
|
name: alias,
|
|
20177
|
-
account: checkedAccount(alias, account)
|
|
20207
|
+
account: checkedAccount(alias, account, platform)
|
|
20178
20208
|
};
|
|
20179
20209
|
}
|
|
20180
20210
|
/** The account under exactly this name, or null: no former names, no refusal. */
|
|
20181
|
-
function lookupAccount(config, name) {
|
|
20211
|
+
function lookupAccount(config, name, platform = process.platform) {
|
|
20182
20212
|
const account = lookupName(config, "account", name);
|
|
20183
20213
|
if (!account || account.platform !== "resend") return null;
|
|
20184
20214
|
return {
|
|
20185
20215
|
name,
|
|
20186
|
-
account: checkedAccount(name, account)
|
|
20216
|
+
account: checkedAccount(name, account, platform)
|
|
20187
20217
|
};
|
|
20188
20218
|
}
|
|
20189
20219
|
/** The account by its immutable id, under whatever name it has now. */
|
|
20190
|
-
function accountById(config, id) {
|
|
20220
|
+
function accountById(config, id, platform = process.platform) {
|
|
20191
20221
|
const found = findById(config, "account", id);
|
|
20192
20222
|
if (!found || found.account.platform !== "resend") return null;
|
|
20193
20223
|
return {
|
|
20194
20224
|
name: found.alias,
|
|
20195
|
-
account: checkedAccount(found.alias, found.account)
|
|
20225
|
+
account: checkedAccount(found.alias, found.account, platform)
|
|
20196
20226
|
};
|
|
20197
20227
|
}
|
|
20198
20228
|
/**
|
|
20199
20229
|
* Refuses a name a new account cannot take — core's rule, for either config version: the grammar and the platform,
|
|
20200
20230
|
* free in both maps, and never a former name — with this package's own hint for one already connected.
|
|
20201
20231
|
*/
|
|
20202
|
-
function checkNewName(config, name) {
|
|
20232
|
+
function checkNewName(config, name, platform = process.platform) {
|
|
20203
20233
|
const check = nameAvailable(config, "account", name, PLATFORM);
|
|
20204
20234
|
if (check.ok) return;
|
|
20205
|
-
if (lookupName(config, "account", name) || lookupName(config, "inbox", name)) throw new CommsError("USAGE", `there is already an account called "${name}"`, { hint: `Choose another name, or remove that one first with
|
|
20235
|
+
if (lookupName(config, "account", name) || lookupName(config, "inbox", name)) throw new CommsError("USAGE", `there is already an account called "${name}"`, { hint: `Choose another name, or remove that one first with ${inlineCommand(shellCommand([
|
|
20236
|
+
"agent-resend",
|
|
20237
|
+
"account",
|
|
20238
|
+
"remove",
|
|
20239
|
+
name
|
|
20240
|
+
], platform))}.` });
|
|
20206
20241
|
throw check.error;
|
|
20207
20242
|
}
|
|
20208
20243
|
/** `config` with `account` under `name`. */
|
|
@@ -20238,20 +20273,22 @@ function changePolicyIn(config, account) {
|
|
|
20238
20273
|
*/
|
|
20239
20274
|
var AccountStore = class {
|
|
20240
20275
|
#load;
|
|
20241
|
-
|
|
20276
|
+
#platform;
|
|
20277
|
+
constructor(load, platform = process.platform) {
|
|
20242
20278
|
this.#load = load;
|
|
20279
|
+
this.#platform = platform;
|
|
20243
20280
|
}
|
|
20244
20281
|
async list() {
|
|
20245
|
-
return resendAccounts(await this.#load());
|
|
20282
|
+
return resendAccounts(await this.#load(), this.#platform);
|
|
20246
20283
|
}
|
|
20247
20284
|
async find(name) {
|
|
20248
|
-
return lookupAccount(await this.#load(), name);
|
|
20285
|
+
return lookupAccount(await this.#load(), name, this.#platform);
|
|
20249
20286
|
}
|
|
20250
20287
|
async findById(id) {
|
|
20251
|
-
return accountById(await this.#load(), id);
|
|
20288
|
+
return accountById(await this.#load(), id, this.#platform);
|
|
20252
20289
|
}
|
|
20253
20290
|
async require(name) {
|
|
20254
|
-
return requireAccount(await this.#load(), name);
|
|
20291
|
+
return requireAccount(await this.#load(), name, this.#platform);
|
|
20255
20292
|
}
|
|
20256
20293
|
};
|
|
20257
20294
|
var Throttle = class {
|
|
@@ -20386,6 +20423,7 @@ var ResendContext = class {
|
|
|
20386
20423
|
core;
|
|
20387
20424
|
env;
|
|
20388
20425
|
now;
|
|
20426
|
+
platform;
|
|
20389
20427
|
surface;
|
|
20390
20428
|
accounts;
|
|
20391
20429
|
#fetch;
|
|
@@ -20395,8 +20433,9 @@ var ResendContext = class {
|
|
|
20395
20433
|
this.env = options.env ?? process.env;
|
|
20396
20434
|
this.core = options.core ?? openCore({ env: this.env });
|
|
20397
20435
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
20436
|
+
this.platform = options.platform ?? process.platform;
|
|
20398
20437
|
this.surface = options.surface ?? "cli";
|
|
20399
|
-
this.accounts = new AccountStore(() => this.core.config.load());
|
|
20438
|
+
this.accounts = new AccountStore(() => this.core.config.load(), this.platform);
|
|
20400
20439
|
this.#fetch = options.fetch;
|
|
20401
20440
|
this.#throttle = new Throttle(this.core.paths.stateDir, options.throttle);
|
|
20402
20441
|
}
|
|
@@ -20445,7 +20484,12 @@ var ResendContext = class {
|
|
|
20445
20484
|
/** A transport for a connected account: its key from the secret store, the machine's throttle, and any open permit. */
|
|
20446
20485
|
async transport(named, permit) {
|
|
20447
20486
|
const key = await (await this.secrets()).get(named.account.secretRef);
|
|
20448
|
-
if (key === null || key.trim() === "") throw new CommsError("AUTH_REQUIRED", `the key for "${named.name}" is not in the secret store`, { hint: `A person removes the account and adds it again:
|
|
20487
|
+
if (key === null || key.trim() === "") throw new CommsError("AUTH_REQUIRED", `the key for "${named.name}" is not in the secret store`, { hint: `A person removes the account and adds it again: ${inlineCommand(shellCommand([
|
|
20488
|
+
"agent-resend",
|
|
20489
|
+
"account",
|
|
20490
|
+
"add",
|
|
20491
|
+
named.name
|
|
20492
|
+
], this.platform))}.` });
|
|
20449
20493
|
return {
|
|
20450
20494
|
fetch: this.#fetch,
|
|
20451
20495
|
key,
|
|
@@ -20593,15 +20637,17 @@ function codeFor(status, name) {
|
|
|
20593
20637
|
message: "Resend refused the request"
|
|
20594
20638
|
};
|
|
20595
20639
|
}
|
|
20596
|
-
|
|
20597
|
-
|
|
20598
|
-
|
|
20599
|
-
|
|
20600
|
-
|
|
20601
|
-
|
|
20602
|
-
|
|
20603
|
-
|
|
20604
|
-
|
|
20640
|
+
const REFUSED_BEFORE_ACTING = /* @__PURE__ */ new Set([
|
|
20641
|
+
400,
|
|
20642
|
+
401,
|
|
20643
|
+
403,
|
|
20644
|
+
404,
|
|
20645
|
+
422,
|
|
20646
|
+
429
|
|
20647
|
+
]);
|
|
20648
|
+
/** Whether a failed write can have taken effect. */
|
|
20649
|
+
function writeOutcomeOf(status) {
|
|
20650
|
+
return status !== void 0 && REFUSED_BEFORE_ACTING.has(status) ? "not-sent" : "unknown";
|
|
20605
20651
|
}
|
|
20606
20652
|
/** One API request. Resolves to the parsed JSON body of a 2xx, or throws a `CommsError` whose text holds no key. */
|
|
20607
20653
|
async function resendRequest(transport, method, path, options = {}) {
|
|
@@ -20674,7 +20720,7 @@ async function resendRequest(transport, method, path, options = {}) {
|
|
|
20674
20720
|
resendError: name,
|
|
20675
20721
|
stage: "http",
|
|
20676
20722
|
...stopFor !== null ? { retryAfterSeconds: stopFor } : {},
|
|
20677
|
-
...writing ? { outcome:
|
|
20723
|
+
...writing ? { outcome: writeOutcomeOf(response.status) } : {}
|
|
20678
20724
|
}
|
|
20679
20725
|
});
|
|
20680
20726
|
}
|
|
@@ -21020,11 +21066,11 @@ function viewOf(named, config) {
|
|
|
21020
21066
|
}
|
|
21021
21067
|
async function listAccounts(context) {
|
|
21022
21068
|
const config = await context.config();
|
|
21023
|
-
return { accounts: resendAccounts(config).map((named) => viewOf(named, config)) };
|
|
21069
|
+
return { accounts: resendAccounts(config, context.platform).map((named) => viewOf(named, config)) };
|
|
21024
21070
|
}
|
|
21025
21071
|
async function showAccount(context, name) {
|
|
21026
21072
|
const config = await context.config();
|
|
21027
|
-
return viewOf(requireAccount(config, name), config);
|
|
21073
|
+
return viewOf(requireAccount(config, name, context.platform), config);
|
|
21028
21074
|
}
|
|
21029
21075
|
/** A mode word checked by the operation, so both surfaces refuse it in the same words. */
|
|
21030
21076
|
function modeOf(raw) {
|
|
@@ -21043,8 +21089,8 @@ function changePolicyOf(raw) {
|
|
|
21043
21089
|
throw new CommsError("USAGE", `"${String(raw)}" is not a change policy: use chat or confirm`);
|
|
21044
21090
|
}
|
|
21045
21091
|
/** Refuses a key that is already connected, under any name: one key, one account. */
|
|
21046
|
-
function refuseConnectedKey(config, fingerprint) {
|
|
21047
|
-
const same = resendAccounts(config).find((named) => named.account.workspace === fingerprint);
|
|
21092
|
+
function refuseConnectedKey(config, fingerprint, platform) {
|
|
21093
|
+
const same = resendAccounts(config, platform).find((named) => named.account.workspace === fingerprint);
|
|
21048
21094
|
if (same) throw new CommsError("USAGE", `this key is already connected as "${same.name}"`, { hint: "One key, one account. Change that one with `agent-resend account policy`." });
|
|
21049
21095
|
}
|
|
21050
21096
|
/**
|
|
@@ -21079,8 +21125,8 @@ function addAccountChange(context, request, inspection) {
|
|
|
21079
21125
|
...domainLock ? { domainLock } : {}
|
|
21080
21126
|
};
|
|
21081
21127
|
const connect = (config) => {
|
|
21082
|
-
checkNewName(config, request.name);
|
|
21083
|
-
refuseConnectedKey(config, fingerprint);
|
|
21128
|
+
checkNewName(config, request.name, context.platform);
|
|
21129
|
+
refuseConnectedKey(config, fingerprint, context.platform);
|
|
21084
21130
|
return withAccount(config, request.name, account);
|
|
21085
21131
|
};
|
|
21086
21132
|
return {
|
|
@@ -21096,7 +21142,7 @@ function addAccountChange(context, request, inspection) {
|
|
|
21096
21142
|
};
|
|
21097
21143
|
},
|
|
21098
21144
|
apply: async (consent) => {
|
|
21099
|
-
const { store, choosing } = secretsStoreFor(await context.config(), void 0);
|
|
21145
|
+
const { store, choosing } = secretsStoreFor(await context.config(), void 0, context.platform);
|
|
21100
21146
|
const secrets = await context.core.secrets(store);
|
|
21101
21147
|
await secrets.set(account.secretRef, request.key.trim());
|
|
21102
21148
|
let written;
|
|
@@ -21137,7 +21183,7 @@ function addAccountChange(context, request, inspection) {
|
|
|
21137
21183
|
function removeAccountChange(context, name) {
|
|
21138
21184
|
return {
|
|
21139
21185
|
plan: (config) => {
|
|
21140
|
-
const found = requireAccount(config, name);
|
|
21186
|
+
const found = requireAccount(config, name, context.platform);
|
|
21141
21187
|
return {
|
|
21142
21188
|
account: found.name,
|
|
21143
21189
|
before: config,
|
|
@@ -21147,15 +21193,25 @@ function removeAccountChange(context, name) {
|
|
|
21147
21193
|
};
|
|
21148
21194
|
},
|
|
21149
21195
|
apply: async (_consent, request) => {
|
|
21150
|
-
const approved = requireAccount(request.before, name).account;
|
|
21196
|
+
const approved = requireAccount(request.before, name, context.platform).account;
|
|
21151
21197
|
const removed = await withCredentialsLock(context.core.paths.configDir, async () => {
|
|
21152
|
-
const found = requireAccount(await context.config(), name);
|
|
21153
|
-
if (found.account.id !== approved.id) throw new CommsError("CONFIG", `"${name}" changed after its removal was approved, so nothing was removed`, { hint: `Look at it with
|
|
21198
|
+
const found = requireAccount(await context.config(), name, context.platform);
|
|
21199
|
+
if (found.account.id !== approved.id) throw new CommsError("CONFIG", `"${name}" changed after its removal was approved, so nothing was removed`, { hint: `Look at it with ${inlineCommand(shellCommand([
|
|
21200
|
+
"agent-resend",
|
|
21201
|
+
"account",
|
|
21202
|
+
"show",
|
|
21203
|
+
name
|
|
21204
|
+
], context.platform))}, and remove it again if you still want it gone.` });
|
|
21154
21205
|
const secrets = await context.secrets();
|
|
21155
21206
|
await secrets.delete(found.account.secretRef).catch(() => false);
|
|
21156
21207
|
await context.core.config.update((current) => {
|
|
21157
|
-
if (secretsStoreOf(current) !== secrets.kind) throw new CommsError("TRANSIENT", `the secret store changed while "${name}" was being removed`, { hint: `Run
|
|
21158
|
-
|
|
21208
|
+
if (secretsStoreOf(current) !== secrets.kind) throw new CommsError("TRANSIENT", `the secret store changed while "${name}" was being removed`, { hint: `Run ${inlineCommand(shellCommand([
|
|
21209
|
+
"agent-resend",
|
|
21210
|
+
"account",
|
|
21211
|
+
"remove",
|
|
21212
|
+
name
|
|
21213
|
+
], context.platform))} again.` });
|
|
21214
|
+
const held = accountById(current, found.account.id, context.platform);
|
|
21159
21215
|
if (!held) return current;
|
|
21160
21216
|
return withoutAccount(current, held.name);
|
|
21161
21217
|
});
|
|
@@ -21207,7 +21263,7 @@ function reportOf(named, config) {
|
|
|
21207
21263
|
}
|
|
21208
21264
|
async function policyReport(context, name) {
|
|
21209
21265
|
const config = await context.config();
|
|
21210
|
-
return reportOf(requireAccount(config, name), config);
|
|
21266
|
+
return reportOf(requireAccount(config, name, context.platform), config);
|
|
21211
21267
|
}
|
|
21212
21268
|
/**
|
|
21213
21269
|
* Why a policy report refuses the approval it is handed, in the words of the surface it came from, as core's
|
|
@@ -21249,7 +21305,7 @@ function policyChange(context, name, wanted) {
|
|
|
21249
21305
|
].filter(Boolean).join(", ");
|
|
21250
21306
|
return {
|
|
21251
21307
|
plan: (config) => {
|
|
21252
|
-
const found = checked(requireAccount(config, name));
|
|
21308
|
+
const found = checked(requireAccount(config, name, context.platform));
|
|
21253
21309
|
return {
|
|
21254
21310
|
account: found.name,
|
|
21255
21311
|
before: config,
|
|
@@ -21258,10 +21314,10 @@ function policyChange(context, name, wanted) {
|
|
|
21258
21314
|
};
|
|
21259
21315
|
},
|
|
21260
21316
|
apply: async (consent, request) => {
|
|
21261
|
-
const approved = requireAccount(request.before, name).account;
|
|
21317
|
+
const approved = requireAccount(request.before, name, context.platform).account;
|
|
21262
21318
|
let written;
|
|
21263
21319
|
const config = await context.core.config.update((current) => {
|
|
21264
|
-
const held = accountById(current, approved.id);
|
|
21320
|
+
const held = accountById(current, approved.id, context.platform);
|
|
21265
21321
|
if (!held) throw new CommsError("CONFIG", `"${name}" changed while its policy was being set, so nothing was set`, { hint: "Check it with `agent-resend account list`, then set the policy again." });
|
|
21266
21322
|
written = {
|
|
21267
21323
|
name: held.name,
|
|
@@ -21308,7 +21364,17 @@ async function checkAccount(context, named, offline) {
|
|
|
21308
21364
|
name: "key stored",
|
|
21309
21365
|
ok: false,
|
|
21310
21366
|
detail: "the key is missing from the secret store",
|
|
21311
|
-
fix:
|
|
21367
|
+
fix: `${commandText(shellCommand([
|
|
21368
|
+
"agent-resend",
|
|
21369
|
+
"account",
|
|
21370
|
+
"remove",
|
|
21371
|
+
named.name
|
|
21372
|
+
], context.platform))}, then ${commandText(shellCommand([
|
|
21373
|
+
"agent-resend",
|
|
21374
|
+
"account",
|
|
21375
|
+
"add",
|
|
21376
|
+
named.name
|
|
21377
|
+
], context.platform))}`
|
|
21312
21378
|
});
|
|
21313
21379
|
} catch (error) {
|
|
21314
21380
|
checks.push({
|
|
@@ -22355,8 +22421,15 @@ async function ownRecord(context, named, approvalId) {
|
|
|
22355
22421
|
if (!record || approvalKind(record) !== "send" || record.inboxId !== named.account.id) throw new CommsError("NOT_FOUND", `there is no send approval ${approvalId} for "${named.name}"`, { hint: "Approvals last ten minutes. Prepare the send again." });
|
|
22356
22422
|
return record;
|
|
22357
22423
|
}
|
|
22358
|
-
function requireSendMode(named) {
|
|
22359
|
-
if (named.account.mode !== "send") throw new CommsError("SCOPE_MISSING", `"${named.name}" is in read mode, so it sends nothing`, { hint: `A person can allow sending with
|
|
22424
|
+
function requireSendMode(named, platform) {
|
|
22425
|
+
if (named.account.mode !== "send") throw new CommsError("SCOPE_MISSING", `"${named.name}" is in read mode, so it sends nothing`, { hint: `A person can allow sending with ${inlineCommand(shellCommand([
|
|
22426
|
+
"agent-resend",
|
|
22427
|
+
"account",
|
|
22428
|
+
"policy",
|
|
22429
|
+
named.name,
|
|
22430
|
+
"--mode",
|
|
22431
|
+
"send"
|
|
22432
|
+
], platform))}, which is a change they approve.` });
|
|
22360
22433
|
}
|
|
22361
22434
|
/**
|
|
22362
22435
|
* Whether the From domain may send, checked before an approval exists.
|
|
@@ -22423,8 +22496,12 @@ function expectationOf(message) {
|
|
|
22423
22496
|
subject: message.subject
|
|
22424
22497
|
};
|
|
22425
22498
|
}
|
|
22426
|
-
function describePolicy(effective, approvalId, flags) {
|
|
22427
|
-
if (effective === "confirm") return `Policy: confirm${flags.length > 0 ? ` (${flags.join(", ")})` : ""} — a person runs
|
|
22499
|
+
function describePolicy(effective, approvalId, flags, platform) {
|
|
22500
|
+
if (effective === "confirm") return `Policy: confirm${flags.length > 0 ? ` (${flags.join(", ")})` : ""} — a person runs ${inlineCommand(shellCommand([
|
|
22501
|
+
"agent-resend",
|
|
22502
|
+
"approve",
|
|
22503
|
+
approvalId
|
|
22504
|
+
], platform))} at their own terminal before this can go.`;
|
|
22428
22505
|
return "Policy: chat — send only after the user approves this exact preview.";
|
|
22429
22506
|
}
|
|
22430
22507
|
function previewOf(options) {
|
|
@@ -22462,7 +22539,7 @@ function previewOf(options) {
|
|
|
22462
22539
|
thread: message.inReplyTo ? `reply to ${message.inReplyTo}${message.references.length > 1 ? ` (${message.references.length} references)` : ""}` : void 0,
|
|
22463
22540
|
links: options.built.links,
|
|
22464
22541
|
warnings,
|
|
22465
|
-
policy: describePolicy(options.effective, options.approvalId, options.flags)
|
|
22542
|
+
policy: describePolicy(options.effective, options.approvalId, options.flags, options.platform)
|
|
22466
22543
|
});
|
|
22467
22544
|
}
|
|
22468
22545
|
async function attachPolicy(context) {
|
|
@@ -22480,10 +22557,17 @@ async function attachPolicy(context) {
|
|
|
22480
22557
|
*/
|
|
22481
22558
|
async function prepareSend(context, name, input) {
|
|
22482
22559
|
const named = await context.accounts.require(name);
|
|
22483
|
-
requireSendMode(named);
|
|
22560
|
+
requireSendMode(named, context.platform);
|
|
22484
22561
|
const config = await context.config();
|
|
22485
22562
|
const livePolicy = named.account.sendPolicy ?? config.defaults.sendPolicy;
|
|
22486
|
-
if (livePolicy === "never") throw new CommsError("POLICY_NEVER", `sending from ${name} is turned off (policy: never)`, { hint: `A person can change it with
|
|
22563
|
+
if (livePolicy === "never") throw new CommsError("POLICY_NEVER", `sending from ${name} is turned off (policy: never)`, { hint: `A person can change it with ${inlineCommand(shellCommand([
|
|
22564
|
+
"agent-resend",
|
|
22565
|
+
"account",
|
|
22566
|
+
"policy",
|
|
22567
|
+
name,
|
|
22568
|
+
"--send",
|
|
22569
|
+
"confirm"
|
|
22570
|
+
], context.platform))}.` });
|
|
22487
22571
|
const built = await buildMessage(input, {
|
|
22488
22572
|
now: context.now(),
|
|
22489
22573
|
attach: await attachPolicy(context)
|
|
@@ -22535,7 +22619,8 @@ async function prepareSend(context, name, input) {
|
|
|
22535
22619
|
study,
|
|
22536
22620
|
domainNote,
|
|
22537
22621
|
effective,
|
|
22538
|
-
flags: study.flags
|
|
22622
|
+
flags: study.flags,
|
|
22623
|
+
platform: context.platform
|
|
22539
22624
|
}),
|
|
22540
22625
|
policy: livePolicy,
|
|
22541
22626
|
effectivePolicy: effective,
|
|
@@ -22543,7 +22628,11 @@ async function prepareSend(context, name, input) {
|
|
|
22543
22628
|
reach,
|
|
22544
22629
|
expect: record.expect,
|
|
22545
22630
|
expiresAt: record.expiresAt,
|
|
22546
|
-
nextStep: effective === "confirm" ? `Show the preview to the user, then have them run
|
|
22631
|
+
nextStep: effective === "confirm" ? `Show the preview to the user, then have them run ${inlineCommand(shellCommand([
|
|
22632
|
+
"agent-resend",
|
|
22633
|
+
"approve",
|
|
22634
|
+
record.approvalId
|
|
22635
|
+
], context.platform))} in their own terminal. You cannot approve this yourself. Then execute it with the same approval id and the recipients and subject shown.` : "Show the preview to the user verbatim and wait for an explicit yes. Then execute it with the same approval id and the recipients and subject shown above."
|
|
22547
22636
|
};
|
|
22548
22637
|
}
|
|
22549
22638
|
/** The stored message and its attachments' bytes, checked again: a file changed since the preview voids the send. */
|
|
@@ -22606,13 +22695,64 @@ function describeState(state) {
|
|
|
22606
22695
|
switch (state) {
|
|
22607
22696
|
case "used": return "this approval has already been used — the email was sent once, and is not sent again";
|
|
22608
22697
|
case "sending": return "this approval is being sent by another process right now";
|
|
22609
|
-
case "failed": return "the send under this approval
|
|
22698
|
+
case "failed": return "the send under this approval was refused; nothing was sent";
|
|
22610
22699
|
case "unknown": return "a process stopped mid-send under this approval; whether the email went is not known";
|
|
22611
22700
|
case "expired": return "this approval has expired";
|
|
22612
22701
|
case "revoked": return "this approval was cancelled or voided";
|
|
22613
22702
|
default: return `this approval is ${state}`;
|
|
22614
22703
|
}
|
|
22615
22704
|
}
|
|
22705
|
+
function messageOf(error) {
|
|
22706
|
+
return error instanceof Error ? error.message : String(error);
|
|
22707
|
+
}
|
|
22708
|
+
/** Keeps the failure that stopped the send visible, adding only what could not be settled afterwards. */
|
|
22709
|
+
function noSendError(error, unrecorded) {
|
|
22710
|
+
const original = error instanceof CommsError ? error : new CommsError("UNEXPECTED", messageOf(error), { cause: error });
|
|
22711
|
+
const hint = [original.hint, ...unrecorded].filter((part) => part !== void 0);
|
|
22712
|
+
return new CommsError(original.code, original.message, {
|
|
22713
|
+
...hint.length === 0 ? {} : { hint: hint.join(" ") },
|
|
22714
|
+
...original.details === void 0 ? {} : { details: original.details },
|
|
22715
|
+
cause: error
|
|
22716
|
+
});
|
|
22717
|
+
}
|
|
22718
|
+
/** Settles a failure known to have happened before Resend sent anything, without one failed write skipping another. */
|
|
22719
|
+
async function recordNoSend(context, records, options, error) {
|
|
22720
|
+
const unrecorded = [];
|
|
22721
|
+
const said = messageOf(error);
|
|
22722
|
+
try {
|
|
22723
|
+
await records.release(options.accountId, options.approvalId);
|
|
22724
|
+
} catch (failure) {
|
|
22725
|
+
unrecorded.push(`the capacity slot could not be released (${messageOf(failure)})`);
|
|
22726
|
+
}
|
|
22727
|
+
try {
|
|
22728
|
+
await context.core.approvals.complete(options.approvalId, { error: said });
|
|
22729
|
+
} catch (failure) {
|
|
22730
|
+
unrecorded.push(`the approval could not be marked failed (${messageOf(failure)})`);
|
|
22731
|
+
}
|
|
22732
|
+
try {
|
|
22733
|
+
await records.record(options.accountId, {
|
|
22734
|
+
approvalId: options.approvalId,
|
|
22735
|
+
event: "failed",
|
|
22736
|
+
error: said.slice(0, 300)
|
|
22737
|
+
});
|
|
22738
|
+
} catch (failure) {
|
|
22739
|
+
unrecorded.push(`the send record could not record the failure (${messageOf(failure)})`);
|
|
22740
|
+
}
|
|
22741
|
+
try {
|
|
22742
|
+
await context.core.audit.append({
|
|
22743
|
+
inboxId: options.accountId,
|
|
22744
|
+
alias: options.name,
|
|
22745
|
+
operation: "resend.send.execute",
|
|
22746
|
+
outcome: "failed",
|
|
22747
|
+
surface: context.surface,
|
|
22748
|
+
approvalId: options.approvalId,
|
|
22749
|
+
reason: [said.slice(0, 200), ...unrecorded].join("; ")
|
|
22750
|
+
});
|
|
22751
|
+
} catch (failure) {
|
|
22752
|
+
unrecorded.push(`the audit log could not record the failure (${messageOf(failure)})`);
|
|
22753
|
+
}
|
|
22754
|
+
return noSendError(error, unrecorded);
|
|
22755
|
+
}
|
|
22616
22756
|
/**
|
|
22617
22757
|
* Step three, and the only place mail leaves.
|
|
22618
22758
|
*
|
|
@@ -22621,10 +22761,17 @@ function describeState(state) {
|
|
|
22621
22761
|
*/
|
|
22622
22762
|
async function executeSend(context, name, options) {
|
|
22623
22763
|
const named = await context.accounts.require(name);
|
|
22624
|
-
requireSendMode(named);
|
|
22764
|
+
requireSendMode(named, context.platform);
|
|
22625
22765
|
const known = await ownRecord(context, named, options.approvalId);
|
|
22626
22766
|
if (known.state !== "pending" && known.state !== "approved") throw new CommsError("APPROVAL_VOID", `nothing was sent: ${describeState(known.state)}`, {
|
|
22627
|
-
hint: known.state === "
|
|
22767
|
+
hint: known.state === "unknown" ? `Check what happened with ${inlineCommand(shellCommand([
|
|
22768
|
+
"agent-resend",
|
|
22769
|
+
"send",
|
|
22770
|
+
"status",
|
|
22771
|
+
options.approvalId,
|
|
22772
|
+
"--account",
|
|
22773
|
+
name
|
|
22774
|
+
], context.platform))} before anything else.` : "Prepare the send again if it should still go.",
|
|
22628
22775
|
details: {
|
|
22629
22776
|
approvalId: options.approvalId,
|
|
22630
22777
|
state: known.state
|
|
@@ -22647,89 +22794,143 @@ async function executeSend(context, name, options) {
|
|
|
22647
22794
|
inboxSub: named.account.userId,
|
|
22648
22795
|
policy: livePolicy,
|
|
22649
22796
|
expect: options.expect
|
|
22650
|
-
}, {
|
|
22797
|
+
}, {
|
|
22798
|
+
pendingHint: `Ask the user to run ${inlineCommand(shellCommand([
|
|
22799
|
+
"agent-resend",
|
|
22800
|
+
"approve",
|
|
22801
|
+
options.approvalId
|
|
22802
|
+
], context.platform))} in their own terminal, then execute it again with the same approval. You cannot approve it yourself.`,
|
|
22803
|
+
platform: context.platform
|
|
22804
|
+
});
|
|
22651
22805
|
const records = new SendRecords(context.core.paths.stateDir, context.now);
|
|
22652
22806
|
const message = prepared.message;
|
|
22653
22807
|
const recipients = uniqueRecipients(message);
|
|
22808
|
+
const bookkeeping = {
|
|
22809
|
+
name,
|
|
22810
|
+
accountId: named.account.id,
|
|
22811
|
+
approvalId: options.approvalId
|
|
22812
|
+
};
|
|
22654
22813
|
try {
|
|
22655
22814
|
await records.reserve(named.account.id, options.approvalId, config.defaults.sendCaps);
|
|
22656
22815
|
} catch (error) {
|
|
22657
|
-
await context
|
|
22658
|
-
|
|
22816
|
+
throw await recordNoSend(context, records, bookkeeping, error);
|
|
22817
|
+
}
|
|
22818
|
+
try {
|
|
22819
|
+
await records.record(named.account.id, {
|
|
22820
|
+
approvalId: options.approvalId,
|
|
22821
|
+
event: "attempt",
|
|
22822
|
+
recipients,
|
|
22823
|
+
subject: message.subject,
|
|
22824
|
+
scheduledAt: message.scheduledAt
|
|
22825
|
+
});
|
|
22826
|
+
await context.core.audit.append({
|
|
22827
|
+
inboxId: named.account.id,
|
|
22828
|
+
alias: name,
|
|
22829
|
+
operation: "resend.send.execute",
|
|
22830
|
+
outcome: "started",
|
|
22831
|
+
surface: context.surface,
|
|
22832
|
+
approvalId: options.approvalId,
|
|
22833
|
+
recipients: recipients.map(canonicalAddress)
|
|
22834
|
+
}, { durable: true });
|
|
22835
|
+
} catch (error) {
|
|
22836
|
+
throw await recordNoSend(context, records, bookkeeping, error);
|
|
22659
22837
|
}
|
|
22660
|
-
await records.record(named.account.id, {
|
|
22661
|
-
approvalId: options.approvalId,
|
|
22662
|
-
event: "attempt",
|
|
22663
|
-
recipients,
|
|
22664
|
-
subject: message.subject,
|
|
22665
|
-
scheduledAt: message.scheduledAt
|
|
22666
|
-
});
|
|
22667
|
-
await context.core.audit.append({
|
|
22668
|
-
inboxId: named.account.id,
|
|
22669
|
-
alias: name,
|
|
22670
|
-
operation: "resend.send.execute",
|
|
22671
|
-
outcome: "started",
|
|
22672
|
-
surface: context.surface,
|
|
22673
|
-
approvalId: options.approvalId,
|
|
22674
|
-
recipients: recipients.map(canonicalAddress)
|
|
22675
|
-
}, { durable: true });
|
|
22676
22838
|
let resendId;
|
|
22839
|
+
let requestIssued = false;
|
|
22840
|
+
const innerFetch = transport.fetch ?? fetch;
|
|
22841
|
+
const trackedTransport = {
|
|
22842
|
+
...transport,
|
|
22843
|
+
fetch: async (...args) => {
|
|
22844
|
+
requestIssued = true;
|
|
22845
|
+
return innerFetch(...args);
|
|
22846
|
+
}
|
|
22847
|
+
};
|
|
22677
22848
|
try {
|
|
22678
|
-
const response = await spendOn(permit, options.approvalId, "emails.send", () => resendRequest(
|
|
22849
|
+
const response = await spendOn(permit, options.approvalId, "emails.send", () => resendRequest(trackedTransport, "POST", "/emails", {
|
|
22679
22850
|
body: payloadOf(message, options.approvalId, contents),
|
|
22680
22851
|
idempotencyKey: options.approvalId
|
|
22681
22852
|
}));
|
|
22682
22853
|
if (typeof response.id !== "string" || response.id === "") throw new CommsError("PROVIDER_UNAVAILABLE", "Resend accepted the send but returned no email id", { details: { outcome: "unknown" } });
|
|
22683
22854
|
resendId = response.id;
|
|
22684
22855
|
} catch (error) {
|
|
22856
|
+
if (!requestIssued) throw await recordNoSend(context, records, bookkeeping, error);
|
|
22685
22857
|
const outcome = error instanceof CommsError && error.details?.outcome === "not-sent" ? "not-sent" : error instanceof CommsError && error.code === "SEND_REFUSED" ? "not-sent" : "unknown";
|
|
22686
22858
|
const said = error instanceof Error ? error.message : String(error);
|
|
22687
|
-
|
|
22688
|
-
|
|
22859
|
+
if (outcome === "not-sent") throw await recordNoSend(context, records, bookkeeping, error);
|
|
22860
|
+
const unrecorded = [];
|
|
22861
|
+
try {
|
|
22862
|
+
await records.record(named.account.id, {
|
|
22863
|
+
approvalId: options.approvalId,
|
|
22864
|
+
event: "unknown",
|
|
22865
|
+
error: said.slice(0, 300)
|
|
22866
|
+
});
|
|
22867
|
+
} catch (failure) {
|
|
22868
|
+
unrecorded.push(`the send record could not record this (${failure instanceof Error ? failure.message : String(failure)})`);
|
|
22869
|
+
}
|
|
22870
|
+
try {
|
|
22871
|
+
await context.core.audit.append({
|
|
22872
|
+
inboxId: named.account.id,
|
|
22873
|
+
alias: name,
|
|
22874
|
+
operation: "resend.send.execute",
|
|
22875
|
+
outcome: "failed",
|
|
22876
|
+
surface: context.surface,
|
|
22877
|
+
approvalId: options.approvalId,
|
|
22878
|
+
reason: `outcome unknown: ${said.slice(0, 200)}`
|
|
22879
|
+
});
|
|
22880
|
+
} catch (failure) {
|
|
22881
|
+
unrecorded.push(`the audit log could not record this (${failure instanceof Error ? failure.message : String(failure)})`);
|
|
22882
|
+
}
|
|
22883
|
+
throw new CommsError("TRANSIENT", `whether the email was sent is not known: ${said}`, {
|
|
22884
|
+
hint: [`Do not send it again. Check the Resend dashboard or ask the recipient, and check with ${inlineCommand(shellCommand([
|
|
22885
|
+
"agent-resend",
|
|
22886
|
+
"send",
|
|
22887
|
+
"status",
|
|
22888
|
+
options.approvalId,
|
|
22889
|
+
"--account",
|
|
22890
|
+
name
|
|
22891
|
+
], context.platform))}; this approval is not used again.`, ...unrecorded].join(" "),
|
|
22892
|
+
details: {
|
|
22893
|
+
...error instanceof CommsError ? error.details : {},
|
|
22894
|
+
approvalId: options.approvalId,
|
|
22895
|
+
outcome: "unknown"
|
|
22896
|
+
},
|
|
22897
|
+
cause: error
|
|
22898
|
+
});
|
|
22899
|
+
}
|
|
22900
|
+
const unrecorded = [];
|
|
22901
|
+
try {
|
|
22902
|
+
await context.core.approvals.complete(options.approvalId, { sentMessageId: resendId });
|
|
22903
|
+
} catch (error) {
|
|
22904
|
+
unrecorded.push(`the approval could not be marked used (${error instanceof Error ? error.message : String(error)}), so it will read as unknown`);
|
|
22905
|
+
}
|
|
22906
|
+
try {
|
|
22689
22907
|
await records.record(named.account.id, {
|
|
22690
22908
|
approvalId: options.approvalId,
|
|
22691
|
-
event:
|
|
22692
|
-
|
|
22909
|
+
event: "sent",
|
|
22910
|
+
resendId
|
|
22693
22911
|
});
|
|
22912
|
+
} catch (error) {
|
|
22913
|
+
unrecorded.push(`the send record could not record it (${error instanceof Error ? error.message : String(error)})`);
|
|
22914
|
+
}
|
|
22915
|
+
try {
|
|
22694
22916
|
await context.core.audit.append({
|
|
22695
22917
|
inboxId: named.account.id,
|
|
22696
22918
|
alias: name,
|
|
22697
22919
|
operation: "resend.send.execute",
|
|
22698
|
-
outcome: "
|
|
22920
|
+
outcome: "ok",
|
|
22699
22921
|
surface: context.surface,
|
|
22700
22922
|
approvalId: options.approvalId,
|
|
22701
|
-
|
|
22923
|
+
ids: { resendIds: [resendId] },
|
|
22924
|
+
recipients: [
|
|
22925
|
+
...claimed.expect.to,
|
|
22926
|
+
...claimed.expect.cc,
|
|
22927
|
+
...claimed.expect.bcc
|
|
22928
|
+
].map(canonicalAddress),
|
|
22929
|
+
reason: [`digest ${digest.slice(0, 12)} · policy ${livePolicy} · ${claimed.approvedVia ?? "chat"}`, ...unrecorded].join(" · ")
|
|
22702
22930
|
});
|
|
22703
|
-
|
|
22704
|
-
|
|
22705
|
-
details: {
|
|
22706
|
-
approvalId: options.approvalId,
|
|
22707
|
-
outcome: "unknown"
|
|
22708
|
-
}
|
|
22709
|
-
});
|
|
22710
|
-
throw error;
|
|
22931
|
+
} catch (error) {
|
|
22932
|
+
unrecorded.push(`the audit log could not record it (${error instanceof Error ? error.message : String(error)})`);
|
|
22711
22933
|
}
|
|
22712
|
-
await context.core.approvals.complete(options.approvalId, { sentMessageId: resendId });
|
|
22713
|
-
await records.record(named.account.id, {
|
|
22714
|
-
approvalId: options.approvalId,
|
|
22715
|
-
event: "sent",
|
|
22716
|
-
resendId
|
|
22717
|
-
});
|
|
22718
|
-
await context.core.audit.append({
|
|
22719
|
-
inboxId: named.account.id,
|
|
22720
|
-
alias: name,
|
|
22721
|
-
operation: "resend.send.execute",
|
|
22722
|
-
outcome: "ok",
|
|
22723
|
-
surface: context.surface,
|
|
22724
|
-
approvalId: options.approvalId,
|
|
22725
|
-
ids: { resendIds: [resendId] },
|
|
22726
|
-
recipients: [
|
|
22727
|
-
...claimed.expect.to,
|
|
22728
|
-
...claimed.expect.cc,
|
|
22729
|
-
...claimed.expect.bcc
|
|
22730
|
-
].map(canonicalAddress),
|
|
22731
|
-
reason: `digest ${digest.slice(0, 12)} · policy ${livePolicy} · ${claimed.approvedVia ?? "chat"}`
|
|
22732
|
-
});
|
|
22733
22934
|
return {
|
|
22734
22935
|
account: name,
|
|
22735
22936
|
approvalId: options.approvalId,
|
|
@@ -22739,7 +22940,8 @@ async function executeSend(context, name, options) {
|
|
|
22739
22940
|
to: claimed.expect.to,
|
|
22740
22941
|
cc: claimed.expect.cc,
|
|
22741
22942
|
bcc: claimed.expect.bcc,
|
|
22742
|
-
subject: claimed.expect.subject
|
|
22943
|
+
subject: claimed.expect.subject,
|
|
22944
|
+
...unrecorded.length > 0 ? { note: unrecorded.join("; ") } : {}
|
|
22743
22945
|
};
|
|
22744
22946
|
}
|
|
22745
22947
|
/**
|
|
@@ -22835,7 +23037,7 @@ async function beginSendApproval(context, approvalId) {
|
|
|
22835
23037
|
const config = await context.config();
|
|
22836
23038
|
const study = await studyRecipients(context, named, prepared.message, config.defaults.riskEscalation);
|
|
22837
23039
|
const effective = stricterPolicy(named.account.sendPolicy ?? config.defaults.sendPolicy, record.requiredPolicy);
|
|
22838
|
-
const challenge = await context.core.approvals.issueChallenge(approvalId);
|
|
23040
|
+
const challenge = await context.core.approvals.issueChallenge(approvalId, "send", context.platform);
|
|
22839
23041
|
return {
|
|
22840
23042
|
approvalId,
|
|
22841
23043
|
account: named.name,
|
|
@@ -22850,7 +23052,8 @@ async function beginSendApproval(context, approvalId) {
|
|
|
22850
23052
|
study,
|
|
22851
23053
|
domainNote: prepared.warnings[0] ?? "",
|
|
22852
23054
|
effective,
|
|
22853
|
-
flags: record.riskFlags
|
|
23055
|
+
flags: record.riskFlags,
|
|
23056
|
+
platform: context.platform
|
|
22854
23057
|
}),
|
|
22855
23058
|
challenge
|
|
22856
23059
|
};
|
|
@@ -22861,7 +23064,7 @@ async function finishSendApproval(context, approvalId, answer) {
|
|
|
22861
23064
|
const approved = await context.core.approvals.approve(approvalId, "terminal", {
|
|
22862
23065
|
draftMessageId: digest,
|
|
22863
23066
|
digest
|
|
22864
|
-
}, answer);
|
|
23067
|
+
}, answer, "send", context.platform);
|
|
22865
23068
|
await context.core.audit.append({
|
|
22866
23069
|
inboxId: named.account.id,
|
|
22867
23070
|
alias: named.name,
|
|
@@ -22887,4 +23090,4 @@ async function revokeSendApproval(context, approvalId) {
|
|
|
22887
23090
|
});
|
|
22888
23091
|
}
|
|
22889
23092
|
//#endregion
|
|
22890
|
-
export {
|
|
23093
|
+
export { inlineCommand as $, CHANGE_POLICIES as A, agentMarker as B, policyChange as C, sendPolicyOf as D, removeAccountChange as E, CHANNEL_SERVERS as F, changeToolResult as G, approvalsOf as H, CommsError as I, commandPathOf as J, checkForUpdates as K, EXIT_CODES as L, SEND_POLICIES as M, checkNewName as N, showAccount as O, strictToolArguments as P, gatedChangeAtTerminal as Q, SEND_LOOKUP as R, policyApprovalRefusal as S, policyWanted as T, approveChangeAtTerminal as U, approvalKind as V, canPrompt as W, exemptFromUpdateGate as X, escapeForDisplay as Y, gatedChange as Z, runDoctor as _, REFUSED as _t, revokeSendApproval as a, renderPrune as at, listAccounts as b, ROUTES as bt, downloadReceived as c, serverInstallChange as ct, listReceived as d, terminalUpdateHooks as dt, installExitStatus as et, listScheduled as f, toCommsError as ft, showSentEmail as g, writeResult as gt, showReceived as h, updateToolGate as ht, prepareSend as i, renderInstall as it, MODES as j, ResendContext as k, getMetrics as l, serverPruneChange as lt, listSuppressions as m, updateGateAtTerminal as mt, executeSend as n, paint as nt, sendStatus as o, runCommand as ot, listSentEmails as p, truncateDisplay as pt, colorEnabled as q, finishSendApproval as r, refuseUnclaimedApproval as rt, cancelScheduledChange as s, runUpdateCheckChild as st, beginSendApproval as t, openCore as tt, listDomains as u, shellCommand as ut, addAccountChange as v, RESEND_API_ORIGIN as vt, policyReport as w, modeOf as x, VERSION$1 as xt, inspectKey as y, RESEND_DOWNLOAD_ORIGIN as yt, UPDATE_CHECK_CHILD_COMMAND as z };
|