@tinycloud/cli 1.3.0-beta.49 → 1.3.0-beta.50
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/courier-entry.js +710 -52
- package/dist/courier-entry.js.map +1 -1
- package/dist/index.js +1211 -26
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +1241 -61
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +7 -6
- package/skills/tc-cli/REFERENCE.md +2 -0
- package/skills/tc-cli/SKILL.md +3 -0
package/dist/courier-entry.js
CHANGED
|
@@ -4,10 +4,10 @@ var EXT = "https://tinycloud.xyz/ext/courier/v0";
|
|
|
4
4
|
var DEFAULT_HOST = "https://tee.node.tinycloud.xyz";
|
|
5
5
|
var COURIER_SPACE = "agents";
|
|
6
6
|
var AGENT_GRANT_EXPIRY = "30d";
|
|
7
|
-
var kv = (
|
|
7
|
+
var kv = (path21, actions) => ({
|
|
8
8
|
service: "tinycloud.kv",
|
|
9
9
|
space: COURIER_SPACE,
|
|
10
|
-
path: `${PREFIX}/${
|
|
10
|
+
path: `${PREFIX}/${path21}`,
|
|
11
11
|
actions: actions.map((action) => `tinycloud.kv/${action}`)
|
|
12
12
|
});
|
|
13
13
|
var AGENT_GRANT = [
|
|
@@ -826,7 +826,7 @@ function authorityTuples(entries) {
|
|
|
826
826
|
return tuples;
|
|
827
827
|
}
|
|
828
828
|
function renderTuples(tuples) {
|
|
829
|
-
const shown = tuples.slice(0, 3).map(({ service, space, path:
|
|
829
|
+
const shown = tuples.slice(0, 3).map(({ service, space, path: path21, action }) => `${service} ${space} ${path21 || '""'} ${action}`);
|
|
830
830
|
if (tuples.length > 3)
|
|
831
831
|
shown.push(`(+${tuples.length - 3} more)`);
|
|
832
832
|
return shown.join("; ");
|
|
@@ -6544,24 +6544,9 @@ async function cmdRevoke(args) {
|
|
|
6544
6544
|
console.log(`Revoked ${handle} (${peer.courierDid}).
|
|
6545
6545
|
Note: TC-721 prevents immediate revocation; access ends when the current outbound delegation expires (${peer.outbound.at(-1)?.expiresAt ?? "unknown"}). Rotating the agent key does not invalidate the old grant; it remains valid until its expiry.`);
|
|
6546
6546
|
}
|
|
6547
|
-
async function
|
|
6548
|
-
const {
|
|
6549
|
-
const peerHandle = args.positional[0] ?? fail2("usage", sendUsage());
|
|
6550
|
-
const peer = getPeer(profile, peerHandle);
|
|
6551
|
-
const which = oneOf(args, ["new", "thread"]);
|
|
6552
|
-
const threadId = which === "new" ? ulid() : which === "thread" ? requireFlag(args, "thread", sendUsage()) : fail2("usage", sendUsage());
|
|
6553
|
-
const subject = args.get("new");
|
|
6554
|
-
const intentRaw = args.get("intent") ?? "request";
|
|
6555
|
-
if (!INTENTS.includes(intentRaw))
|
|
6556
|
-
fail2("usage", `--intent must be one of: ${INTENTS.join(", ")}`);
|
|
6557
|
-
const text = readTextArg(args, "--text or --file is required (or pipe the body on stdin)");
|
|
6558
|
-
if (text.length > 256 * 1024)
|
|
6559
|
-
fail2("too-large", "Message body exceeds the 256 KB inline limit (\xA74) \u2014 put it under files/ instead.");
|
|
6547
|
+
async function sendMessage(node, profile, peerHandle, peer, input) {
|
|
6548
|
+
const { which, threadId, subject, text, intent, asks, constraints, inReplyTo, referenceTaskIds } = input;
|
|
6560
6549
|
const from = { owner: profile.ownerDid, agent: profile.courierDid, label: profile.label };
|
|
6561
|
-
const asks = args.all("ask").map((title, i) => ({ id: `a${i + 1}`, title }));
|
|
6562
|
-
const constraints = args.all("constraint");
|
|
6563
|
-
const inReplyTo = args.get("reply-to") ?? null;
|
|
6564
|
-
const referenceTaskIds = args.all("task-ref");
|
|
6565
6550
|
const startedByMe = which === "new" || Boolean(await getJson(node.kv, k.threadMeta(peerHandle, threadId)));
|
|
6566
6551
|
let refsOwnTask = false;
|
|
6567
6552
|
let refsAnyTask = false;
|
|
@@ -6582,7 +6567,7 @@ async function cmdSend(args) {
|
|
|
6582
6567
|
threadId,
|
|
6583
6568
|
text,
|
|
6584
6569
|
from,
|
|
6585
|
-
intent
|
|
6570
|
+
intent,
|
|
6586
6571
|
asks,
|
|
6587
6572
|
constraints,
|
|
6588
6573
|
inReplyTo,
|
|
@@ -6599,6 +6584,32 @@ async function cmdSend(args) {
|
|
|
6599
6584
|
await putCreate(node.kv, k.threadMeta(peerHandle, threadId), meta);
|
|
6600
6585
|
}
|
|
6601
6586
|
await putCreate(node.kv, k.msg(peerHandle, threadId, msg.messageId), msg);
|
|
6587
|
+
return msg;
|
|
6588
|
+
}
|
|
6589
|
+
async function cmdSend(args) {
|
|
6590
|
+
const { profile, node } = await ctx();
|
|
6591
|
+
const peerHandle = args.positional[0] ?? fail2("usage", sendUsage());
|
|
6592
|
+
const peer = getPeer(profile, peerHandle);
|
|
6593
|
+
const which = oneOf(args, ["new", "thread"]);
|
|
6594
|
+
const threadId = which === "new" ? ulid() : which === "thread" ? requireFlag(args, "thread", sendUsage()) : fail2("usage", sendUsage());
|
|
6595
|
+
const subject = args.get("new");
|
|
6596
|
+
const intentRaw = args.get("intent") ?? "request";
|
|
6597
|
+
if (!INTENTS.includes(intentRaw))
|
|
6598
|
+
fail2("usage", `--intent must be one of: ${INTENTS.join(", ")}`);
|
|
6599
|
+
const text = readTextArg(args, "--text or --file is required (or pipe the body on stdin)");
|
|
6600
|
+
if (text.length > 256 * 1024)
|
|
6601
|
+
fail2("too-large", "Message body exceeds the 256 KB inline limit (\xA74) \u2014 put it under files/ instead.");
|
|
6602
|
+
const msg = await sendMessage(node, profile, peerHandle, peer, {
|
|
6603
|
+
which: which === "new" ? "new" : "thread",
|
|
6604
|
+
threadId,
|
|
6605
|
+
subject,
|
|
6606
|
+
text,
|
|
6607
|
+
intent: intentRaw,
|
|
6608
|
+
asks: args.all("ask").map((title, i) => ({ id: `a${i + 1}`, title })),
|
|
6609
|
+
constraints: args.all("constraint"),
|
|
6610
|
+
inReplyTo: args.get("reply-to") ?? null,
|
|
6611
|
+
referenceTaskIds: args.all("task-ref")
|
|
6612
|
+
});
|
|
6602
6613
|
printJson({ ok: true, threadId, messageId: msg.messageId, peer: peerHandle, intent: intentRaw, role: msg.role });
|
|
6603
6614
|
}
|
|
6604
6615
|
var sendUsage = () => `Usage: ${cmd('send <peer> (--new <subject> | --thread <id>) [--text t | --file f | stdin] [--intent i] [--ask "\u2026"]\u2026 [--reply-to id]')}`;
|
|
@@ -7384,8 +7395,8 @@ var Watcher = class {
|
|
|
7384
7395
|
const prefix = `${PREFIX}/to/${peer.handle}/threads/`;
|
|
7385
7396
|
const keys = await listAll(this.deps.selfKv, prefix);
|
|
7386
7397
|
const known = /* @__PURE__ */ new Set();
|
|
7387
|
-
for (const
|
|
7388
|
-
const match = /^([^/]+)\/(?:meta\.json|(?:msg|task)\/[^/]+\.json)$/.exec(
|
|
7398
|
+
for (const path21 of keys) {
|
|
7399
|
+
const match = /^([^/]+)\/(?:meta\.json|(?:msg|task)\/[^/]+\.json)$/.exec(path21.slice(prefix.length));
|
|
7389
7400
|
if (match && SAFE_ID.test(match[1]))
|
|
7390
7401
|
known.add(`${peer.handle}/${match[1]}`);
|
|
7391
7402
|
}
|
|
@@ -9043,35 +9054,35 @@ function parseEntry(value2) {
|
|
|
9043
9054
|
return void 0;
|
|
9044
9055
|
return { tier, ratePerHour, outPerHour };
|
|
9045
9056
|
}
|
|
9046
|
-
function loadPeers(
|
|
9057
|
+
function loadPeers(path21, ownerUid2 = process.getuid?.()) {
|
|
9047
9058
|
let fd;
|
|
9048
9059
|
try {
|
|
9049
|
-
fd = fs14.openSync(
|
|
9060
|
+
fd = fs14.openSync(path21, fs14.constants.O_RDONLY | fs14.constants.O_NOFOLLOW);
|
|
9050
9061
|
} catch (error) {
|
|
9051
9062
|
const code = error.code;
|
|
9052
9063
|
if (code === "ENOENT")
|
|
9053
9064
|
return /* @__PURE__ */ Object.create(null);
|
|
9054
9065
|
if (code === "ELOOP")
|
|
9055
|
-
throw new CourierError("insecure-peers", `${
|
|
9066
|
+
throw new CourierError("insecure-peers", `${path21} is a symbolic link; point the flag at the file itself.`);
|
|
9056
9067
|
throw error;
|
|
9057
9068
|
}
|
|
9058
9069
|
try {
|
|
9059
9070
|
const stat = fs14.fstatSync(fd);
|
|
9060
9071
|
if (!stat.isFile())
|
|
9061
|
-
throw new CourierError("insecure-peers", `${
|
|
9062
|
-
if (
|
|
9063
|
-
throw new CourierError("insecure-peers", `${
|
|
9072
|
+
throw new CourierError("insecure-peers", `${path21} is not a regular file.`);
|
|
9073
|
+
if (ownerUid2 !== void 0 && stat.uid !== ownerUid2)
|
|
9074
|
+
throw new CourierError("insecure-peers", `${path21} is not owned by the courier user.`);
|
|
9064
9075
|
if ((stat.mode & 18) !== 0) {
|
|
9065
|
-
throw new CourierError("insecure-peers", `${
|
|
9076
|
+
throw new CourierError("insecure-peers", `${path21} is writable by group or others; restrict it to the owner (chmod 600).`);
|
|
9066
9077
|
}
|
|
9067
9078
|
const parsed = JSON.parse(fs14.readFileSync(fd, "utf8"));
|
|
9068
9079
|
if (!isRecord(parsed))
|
|
9069
|
-
throw new CourierError("bad-peers", `${
|
|
9080
|
+
throw new CourierError("bad-peers", `${path21} must be a JSON object keyed by peer id.`);
|
|
9070
9081
|
const peers = /* @__PURE__ */ Object.create(null);
|
|
9071
9082
|
for (const [peerId, value2] of Object.entries(parsed)) {
|
|
9072
9083
|
const policy = parseEntry(value2);
|
|
9073
9084
|
if (!policy)
|
|
9074
|
-
throw new CourierError("bad-peers", `${
|
|
9085
|
+
throw new CourierError("bad-peers", `${path21} has an invalid policy for peer "${peerId}"; fix the entry. Nothing was loaded.`);
|
|
9075
9086
|
peers[peerId] = policy;
|
|
9076
9087
|
}
|
|
9077
9088
|
return peers;
|
|
@@ -9103,6 +9114,7 @@ function ensureRealDir(dir, mode) {
|
|
|
9103
9114
|
}
|
|
9104
9115
|
if (!existed && stat.uid === process.getuid?.() && (stat.mode & 4095) !== mode)
|
|
9105
9116
|
fs15.chmodSync(dir, mode);
|
|
9117
|
+
return !existed;
|
|
9106
9118
|
}
|
|
9107
9119
|
function fsyncDir(dir) {
|
|
9108
9120
|
const fd = fs15.openSync(dir, "r");
|
|
@@ -9852,6 +9864,643 @@ async function cmdIntake(args) {
|
|
|
9852
9864
|
}
|
|
9853
9865
|
}
|
|
9854
9866
|
|
|
9867
|
+
// ../courier/dist/outbox.js
|
|
9868
|
+
import fs17 from "fs";
|
|
9869
|
+
import net from "net";
|
|
9870
|
+
import path19 from "path";
|
|
9871
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
9872
|
+
var ULID_RE2 = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/;
|
|
9873
|
+
var PEER_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
9874
|
+
var REQUEST_RE = /^\{"v":1,"itemId":"([0-7][0-9A-HJKMNP-TV-Z]{25})"\}$/;
|
|
9875
|
+
var MAX_REQUEST_BYTES = 64;
|
|
9876
|
+
var MAX_ITEM_BYTES = 32 * 1024;
|
|
9877
|
+
var RATE_WINDOW_MS2 = 60 * 60 * 1e3;
|
|
9878
|
+
var PIPE_TIMEOUT_MS = 5e3;
|
|
9879
|
+
var MAX_CONNECTIONS = 64;
|
|
9880
|
+
var MAX_PENDING = 32;
|
|
9881
|
+
var LOG_WINDOW_MS = 60 * 1e3;
|
|
9882
|
+
var LOG_BUDGET = 10;
|
|
9883
|
+
var REQUEST_BUDGET = 60;
|
|
9884
|
+
var REQUEST_WINDOW_MS = 60 * 1e3;
|
|
9885
|
+
var DEFAULT_SOCKET_GROUP = "courier-sender";
|
|
9886
|
+
var DENIED_SOCKET_GROUPS = ["tinycloud", "tinycloud-dev", "sudo", "docker"];
|
|
9887
|
+
var SERVE_LOCK = "serve.lock";
|
|
9888
|
+
var APPROVED_DIR = "approved";
|
|
9889
|
+
var ITEM_MODE = 384;
|
|
9890
|
+
var ITEM_KEYS = ["v", "itemId", "to", "thread", "subject", "text", "approvedSha256"];
|
|
9891
|
+
var Refusal = class extends Error {
|
|
9892
|
+
code;
|
|
9893
|
+
constructor(code) {
|
|
9894
|
+
super(code);
|
|
9895
|
+
this.code = code;
|
|
9896
|
+
}
|
|
9897
|
+
};
|
|
9898
|
+
var ADD_USAGE = `Usage: ${cmd("outbox add <peer> (--thread <id> | --new <subject>) [--text t | --file f | stdin] [--outbox-dir d]")}`;
|
|
9899
|
+
var SERVE_USAGE = `Usage: ${cmd("outbox serve --pipe <socket> [--outbox-dir d] [--socket-group g] [--peers-file f] [--sent-dir d] [--once] [--dry-run]")}`;
|
|
9900
|
+
var OUTBOX_USAGE = `Usage: ${cmd("outbox add <peer> ... | outbox serve ...")}`;
|
|
9901
|
+
function lstatOrUndefined(file) {
|
|
9902
|
+
try {
|
|
9903
|
+
return fs17.lstatSync(file);
|
|
9904
|
+
} catch (error) {
|
|
9905
|
+
if (error.code === "ENOENT")
|
|
9906
|
+
return void 0;
|
|
9907
|
+
throw error;
|
|
9908
|
+
}
|
|
9909
|
+
}
|
|
9910
|
+
function ownerUid() {
|
|
9911
|
+
const uid = process.getuid?.();
|
|
9912
|
+
if (uid === void 0)
|
|
9913
|
+
throw new CourierError("unsupported", "the outbox needs POSIX file ownership");
|
|
9914
|
+
return uid;
|
|
9915
|
+
}
|
|
9916
|
+
var LOGGABLE_CODES = /* @__PURE__ */ new Set([
|
|
9917
|
+
"EACCES",
|
|
9918
|
+
"EEXIST",
|
|
9919
|
+
"EIO",
|
|
9920
|
+
"EISDIR",
|
|
9921
|
+
"EMFILE",
|
|
9922
|
+
"ENOENT",
|
|
9923
|
+
"ENOSPC",
|
|
9924
|
+
"ENOTDIR",
|
|
9925
|
+
"EPERM",
|
|
9926
|
+
"EPIPE",
|
|
9927
|
+
"EROFS",
|
|
9928
|
+
"ECONNRESET",
|
|
9929
|
+
"bad-marker",
|
|
9930
|
+
"bad-peers",
|
|
9931
|
+
"socket-group-denied",
|
|
9932
|
+
"unknown-group",
|
|
9933
|
+
"unsafe-dir",
|
|
9934
|
+
"unsafe-pipe",
|
|
9935
|
+
"unsafe-socket-dir",
|
|
9936
|
+
"unsafe-socket-group",
|
|
9937
|
+
"unsupported"
|
|
9938
|
+
]);
|
|
9939
|
+
function safeCode(error) {
|
|
9940
|
+
const code = errInfo(error).code;
|
|
9941
|
+
return typeof code === "string" && LOGGABLE_CODES.has(code) ? code : "unknown";
|
|
9942
|
+
}
|
|
9943
|
+
function itemHash(item) {
|
|
9944
|
+
return requestHash(Object.fromEntries(Object.entries(item).filter(([key]) => key !== "approvedSha256")));
|
|
9945
|
+
}
|
|
9946
|
+
function textOk(text, subject) {
|
|
9947
|
+
if (!textVerdict(text, "multiline").allowed)
|
|
9948
|
+
return false;
|
|
9949
|
+
return subject === void 0 || textVerdict(subject, "summary").allowed;
|
|
9950
|
+
}
|
|
9951
|
+
function parseRequest(frame) {
|
|
9952
|
+
if (frame.length > MAX_REQUEST_BYTES)
|
|
9953
|
+
throw new Refusal("bad-request");
|
|
9954
|
+
const match = REQUEST_RE.exec(frame.toString("utf8"));
|
|
9955
|
+
if (match === null)
|
|
9956
|
+
throw new Refusal("bad-request");
|
|
9957
|
+
return match[1];
|
|
9958
|
+
}
|
|
9959
|
+
function findItem(root, itemId) {
|
|
9960
|
+
const hits = [];
|
|
9961
|
+
for (const entry of fs17.readdirSync(root)) {
|
|
9962
|
+
if (entry === APPROVED_DIR)
|
|
9963
|
+
continue;
|
|
9964
|
+
const dir = path19.join(root, entry);
|
|
9965
|
+
if (lstatOrUndefined(dir)?.isDirectory() !== true)
|
|
9966
|
+
continue;
|
|
9967
|
+
const file = path19.join(dir, `${itemId}.json`);
|
|
9968
|
+
if (lstatOrUndefined(file) !== void 0)
|
|
9969
|
+
hits.push({ peer: entry, file });
|
|
9970
|
+
}
|
|
9971
|
+
if (hits.length === 0)
|
|
9972
|
+
throw new Refusal("not-found");
|
|
9973
|
+
if (hits.length > 1)
|
|
9974
|
+
throw new Refusal("ambiguous-item");
|
|
9975
|
+
return hits[0];
|
|
9976
|
+
}
|
|
9977
|
+
function readItemFile(file) {
|
|
9978
|
+
let fd;
|
|
9979
|
+
try {
|
|
9980
|
+
fd = fs17.openSync(file, fs17.constants.O_RDONLY | fs17.constants.O_NOFOLLOW);
|
|
9981
|
+
} catch {
|
|
9982
|
+
throw new Refusal("unsafe-item");
|
|
9983
|
+
}
|
|
9984
|
+
try {
|
|
9985
|
+
const st = fs17.fstatSync(fd);
|
|
9986
|
+
if (!st.isFile() || st.uid !== ownerUid() || (st.mode & 63) !== 0)
|
|
9987
|
+
throw new Refusal("unsafe-item");
|
|
9988
|
+
const buf = Buffer.alloc(MAX_ITEM_BYTES + 1);
|
|
9989
|
+
let total = 0;
|
|
9990
|
+
for (; ; ) {
|
|
9991
|
+
const n = fs17.readSync(fd, buf, total, buf.length - total, total);
|
|
9992
|
+
if (n === 0)
|
|
9993
|
+
break;
|
|
9994
|
+
total += n;
|
|
9995
|
+
if (total > MAX_ITEM_BYTES)
|
|
9996
|
+
throw new Refusal("bad-item");
|
|
9997
|
+
}
|
|
9998
|
+
return buf.subarray(0, total);
|
|
9999
|
+
} finally {
|
|
10000
|
+
fs17.closeSync(fd);
|
|
10001
|
+
}
|
|
10002
|
+
}
|
|
10003
|
+
function readLedger(root, itemId) {
|
|
10004
|
+
const file = path19.join(root, APPROVED_DIR, itemId);
|
|
10005
|
+
let fd;
|
|
10006
|
+
try {
|
|
10007
|
+
fd = fs17.openSync(file, fs17.constants.O_RDONLY | fs17.constants.O_NOFOLLOW);
|
|
10008
|
+
} catch (error) {
|
|
10009
|
+
if (error.code === "ENOENT")
|
|
10010
|
+
return void 0;
|
|
10011
|
+
throw new Refusal("unsafe-item");
|
|
10012
|
+
}
|
|
10013
|
+
try {
|
|
10014
|
+
const st = fs17.fstatSync(fd);
|
|
10015
|
+
if (!st.isFile() || st.uid !== ownerUid() || (st.mode & 63) !== 0 || st.size > 256)
|
|
10016
|
+
throw new Refusal("unsafe-item");
|
|
10017
|
+
return fs17.readFileSync(fd, "utf8").replace(/\n$/, "");
|
|
10018
|
+
} finally {
|
|
10019
|
+
fs17.closeSync(fd);
|
|
10020
|
+
}
|
|
10021
|
+
}
|
|
10022
|
+
function isOutboxItem(value2, itemId, peer) {
|
|
10023
|
+
if (typeof value2 !== "object" || value2 === null || Array.isArray(value2))
|
|
10024
|
+
return false;
|
|
10025
|
+
const o = value2;
|
|
10026
|
+
if (!Object.keys(o).every((key) => ITEM_KEYS.includes(key)))
|
|
10027
|
+
return false;
|
|
10028
|
+
const threadOk = typeof o.thread === "string" && ULID_RE2.test(o.thread) && o.subject === void 0 || o.thread === null && typeof o.subject === "string";
|
|
10029
|
+
return o.v === 1 && o.itemId === itemId && o.to === peer && PEER_RE.test(peer) && typeof o.text === "string" && typeof o.approvedSha256 === "string" && threadOk;
|
|
10030
|
+
}
|
|
10031
|
+
function parseItem(raw, itemId, peer) {
|
|
10032
|
+
let value2;
|
|
10033
|
+
try {
|
|
10034
|
+
const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(raw);
|
|
10035
|
+
value2 = JSON.parse(text);
|
|
10036
|
+
if (text !== `${canonicalJson(value2)}
|
|
10037
|
+
`)
|
|
10038
|
+
throw new Refusal("bad-item");
|
|
10039
|
+
} catch {
|
|
10040
|
+
throw new Refusal("bad-item");
|
|
10041
|
+
}
|
|
10042
|
+
if (!isOutboxItem(value2, itemId, peer))
|
|
10043
|
+
throw new Refusal("bad-item");
|
|
10044
|
+
return value2;
|
|
10045
|
+
}
|
|
10046
|
+
function readMarker(file) {
|
|
10047
|
+
let value2;
|
|
10048
|
+
try {
|
|
10049
|
+
value2 = JSON.parse(fs17.readFileSync(file, "utf8"));
|
|
10050
|
+
} catch {
|
|
10051
|
+
throw new CourierError("bad-marker", "unreadable sent marker");
|
|
10052
|
+
}
|
|
10053
|
+
const m = typeof value2 === "object" && value2 !== null ? value2 : {};
|
|
10054
|
+
const at = typeof m.at === "string" ? Date.parse(m.at) : Number.NaN;
|
|
10055
|
+
if (typeof m.peer !== "string" || Number.isNaN(at))
|
|
10056
|
+
throw new CourierError("bad-marker", "malformed sent marker");
|
|
10057
|
+
return { peer: m.peer, at };
|
|
10058
|
+
}
|
|
10059
|
+
function checkRate(sentDir, peer, cap2, now) {
|
|
10060
|
+
let recent = 0;
|
|
10061
|
+
for (const name2 of fs17.readdirSync(sentDir)) {
|
|
10062
|
+
if (!ULID_RE2.test(name2))
|
|
10063
|
+
continue;
|
|
10064
|
+
const marker = readMarker(path19.join(sentDir, name2));
|
|
10065
|
+
if (marker.peer === peer && now - marker.at < RATE_WINDOW_MS2)
|
|
10066
|
+
recent++;
|
|
10067
|
+
}
|
|
10068
|
+
if (recent >= cap2)
|
|
10069
|
+
throw new Refusal("rate-limited");
|
|
10070
|
+
}
|
|
10071
|
+
function recordResult(sentDir, itemId, state, messageId, now, log) {
|
|
10072
|
+
const body = JSON.stringify({ v: 1, state, at: new Date(now).toISOString(), messageId });
|
|
10073
|
+
try {
|
|
10074
|
+
writeReplace(sentDir, `${itemId}.result.json`, `${body}
|
|
10075
|
+
`, 384);
|
|
10076
|
+
} catch (error) {
|
|
10077
|
+
log("outbox.result-write-failed", { code: safeCode(error) });
|
|
10078
|
+
}
|
|
10079
|
+
}
|
|
10080
|
+
function reconcileUncertain(sentDir, now, log) {
|
|
10081
|
+
const pending = fs17.readdirSync(sentDir).filter((name2) => ULID_RE2.test(name2) && lstatOrUndefined(path19.join(sentDir, `${name2}.result.json`)) === void 0);
|
|
10082
|
+
for (const itemId of pending)
|
|
10083
|
+
recordResult(sentDir, itemId, "uncertain", void 0, now, log);
|
|
10084
|
+
return pending.length;
|
|
10085
|
+
}
|
|
10086
|
+
function recordedState(sentDir, itemId) {
|
|
10087
|
+
const file = path19.join(sentDir, `${itemId}.result.json`);
|
|
10088
|
+
if (lstatOrUndefined(file) === void 0)
|
|
10089
|
+
return void 0;
|
|
10090
|
+
try {
|
|
10091
|
+
const state = JSON.parse(fs17.readFileSync(file, "utf8")).state;
|
|
10092
|
+
return typeof state === "string" ? state : "uncertain";
|
|
10093
|
+
} catch {
|
|
10094
|
+
return "uncertain";
|
|
10095
|
+
}
|
|
10096
|
+
}
|
|
10097
|
+
function groupGid(name2, file = "/etc/group") {
|
|
10098
|
+
for (const line of fs17.readFileSync(file, "utf8").split("\n")) {
|
|
10099
|
+
const [group, , gid] = line.split(":");
|
|
10100
|
+
if (group === name2 && gid !== void 0 && /^\d+$/.test(gid))
|
|
10101
|
+
return Number(gid);
|
|
10102
|
+
}
|
|
10103
|
+
throw new CourierError("unknown-group", `socket group ${name2} does not exist`);
|
|
10104
|
+
}
|
|
10105
|
+
function checkSocketDir(dir, group, gidOf) {
|
|
10106
|
+
const st = lstatOrUndefined(dir);
|
|
10107
|
+
if (!st?.isDirectory())
|
|
10108
|
+
throw new CourierError("unsafe-socket-dir", "socket directory must be a real directory");
|
|
10109
|
+
if (st.uid !== ownerUid())
|
|
10110
|
+
throw new CourierError("unsafe-socket-dir", "socket directory must be owned by this user");
|
|
10111
|
+
if ((st.mode & 511 & ~488) !== 0)
|
|
10112
|
+
throw new CourierError("unsafe-socket-dir", "socket directory mode must not exceed 0750");
|
|
10113
|
+
if (DENIED_SOCKET_GROUPS.includes(group))
|
|
10114
|
+
throw new CourierError("unsafe-socket-group", `group ${group} may not own the sender socket`);
|
|
10115
|
+
const gid = gidOf(group);
|
|
10116
|
+
if (st.gid !== gid)
|
|
10117
|
+
throw new CourierError("unsafe-socket-dir", "socket directory group must equal the configured group");
|
|
10118
|
+
return gid;
|
|
10119
|
+
}
|
|
10120
|
+
function listen(server, pipe) {
|
|
10121
|
+
return new Promise((resolve, reject) => {
|
|
10122
|
+
server.once("error", reject);
|
|
10123
|
+
server.listen(pipe, () => {
|
|
10124
|
+
server.off("error", reject);
|
|
10125
|
+
resolve();
|
|
10126
|
+
});
|
|
10127
|
+
});
|
|
10128
|
+
}
|
|
10129
|
+
function nodeSender() {
|
|
10130
|
+
let node;
|
|
10131
|
+
return async (profile, peerHandle, input) => {
|
|
10132
|
+
node ??= courierNode(profile);
|
|
10133
|
+
return sendMessage(await node, profile, peerHandle, getPeer(profile, peerHandle), input);
|
|
10134
|
+
};
|
|
10135
|
+
}
|
|
10136
|
+
function budgetedLog(log, now) {
|
|
10137
|
+
const budget = { start: 0, used: 0, suppressed: 0 };
|
|
10138
|
+
return (event, fields = {}) => {
|
|
10139
|
+
const t = now();
|
|
10140
|
+
if (t < budget.start || t - budget.start >= LOG_WINDOW_MS) {
|
|
10141
|
+
if (budget.suppressed > 0)
|
|
10142
|
+
log("outbox.log-suppressed", { count: budget.suppressed });
|
|
10143
|
+
budget.start = t;
|
|
10144
|
+
budget.used = 0;
|
|
10145
|
+
budget.suppressed = 0;
|
|
10146
|
+
}
|
|
10147
|
+
if (budget.used < LOG_BUDGET) {
|
|
10148
|
+
budget.used++;
|
|
10149
|
+
log(event, fields);
|
|
10150
|
+
} else {
|
|
10151
|
+
budget.suppressed++;
|
|
10152
|
+
}
|
|
10153
|
+
};
|
|
10154
|
+
}
|
|
10155
|
+
async function startOutbox(opts) {
|
|
10156
|
+
const log = opts.log ?? jsonLineLog();
|
|
10157
|
+
const now = opts.now ?? Date.now;
|
|
10158
|
+
const limited = budgetedLog(log, now);
|
|
10159
|
+
const loadNow = opts.loadProfile ?? loadProfile;
|
|
10160
|
+
const send = opts.send ?? nodeSender();
|
|
10161
|
+
const gidOf = opts.groups ?? groupGid;
|
|
10162
|
+
const dryRun = opts.dryRun ?? false;
|
|
10163
|
+
const once = opts.once ?? false;
|
|
10164
|
+
if (ensureRealDir(opts.sentDir, 448))
|
|
10165
|
+
fsyncDir(path19.dirname(opts.sentDir));
|
|
10166
|
+
const releaseLock = acquireLock(path19.join(opts.sentDir, SERVE_LOCK));
|
|
10167
|
+
const uncertain = reconcileUncertain(opts.sentDir, Date.now(), limited);
|
|
10168
|
+
if (uncertain > 0)
|
|
10169
|
+
limited("outbox.uncertain", { count: uncertain });
|
|
10170
|
+
const server = net.createServer({ allowHalfOpen: true }, accept);
|
|
10171
|
+
try {
|
|
10172
|
+
const gid = checkSocketDir(path19.dirname(opts.pipe), opts.socketGroup, gidOf);
|
|
10173
|
+
const stale = lstatOrUndefined(opts.pipe);
|
|
10174
|
+
if (stale !== void 0) {
|
|
10175
|
+
if (!stale.isSocket())
|
|
10176
|
+
throw new CourierError("unsafe-pipe", "pipe path exists and is not a socket");
|
|
10177
|
+
fs17.unlinkSync(opts.pipe);
|
|
10178
|
+
}
|
|
10179
|
+
await listen(server, opts.pipe);
|
|
10180
|
+
try {
|
|
10181
|
+
fs17.chownSync(opts.pipe, -1, gid);
|
|
10182
|
+
fs17.chmodSync(opts.pipe, 432);
|
|
10183
|
+
} catch {
|
|
10184
|
+
throw new CourierError("socket-group-denied", "could not assign the sender socket to the configured group");
|
|
10185
|
+
}
|
|
10186
|
+
} catch (error) {
|
|
10187
|
+
if (server.listening) {
|
|
10188
|
+
server.close();
|
|
10189
|
+
if (lstatOrUndefined(opts.pipe)?.isSocket())
|
|
10190
|
+
fs17.unlinkSync(opts.pipe);
|
|
10191
|
+
}
|
|
10192
|
+
releaseLock();
|
|
10193
|
+
throw error;
|
|
10194
|
+
}
|
|
10195
|
+
limited("outbox.listening", { once, dryRun });
|
|
10196
|
+
let chain = Promise.resolve();
|
|
10197
|
+
let pending = 0;
|
|
10198
|
+
let open = 0;
|
|
10199
|
+
let finish = () => void 0;
|
|
10200
|
+
const done = new Promise((resolve) => {
|
|
10201
|
+
finish = resolve;
|
|
10202
|
+
});
|
|
10203
|
+
let closing;
|
|
10204
|
+
const close = () => {
|
|
10205
|
+
closing ??= (async () => {
|
|
10206
|
+
await new Promise((resolve) => {
|
|
10207
|
+
server.close(() => resolve());
|
|
10208
|
+
});
|
|
10209
|
+
await chain;
|
|
10210
|
+
if (lstatOrUndefined(opts.pipe)?.isSocket())
|
|
10211
|
+
fs17.unlinkSync(opts.pipe);
|
|
10212
|
+
releaseLock();
|
|
10213
|
+
finish();
|
|
10214
|
+
})();
|
|
10215
|
+
return closing;
|
|
10216
|
+
};
|
|
10217
|
+
const requests = { start: 0, count: 0 };
|
|
10218
|
+
function admitRequest() {
|
|
10219
|
+
const t = now();
|
|
10220
|
+
if (t < requests.start || t - requests.start >= REQUEST_WINDOW_MS) {
|
|
10221
|
+
requests.start = t;
|
|
10222
|
+
requests.count = 0;
|
|
10223
|
+
}
|
|
10224
|
+
if (requests.count >= REQUEST_BUDGET)
|
|
10225
|
+
return false;
|
|
10226
|
+
requests.count++;
|
|
10227
|
+
return true;
|
|
10228
|
+
}
|
|
10229
|
+
const UNLOADED_OUT_PER_HOUR = 1;
|
|
10230
|
+
let peersNow;
|
|
10231
|
+
function currentPeers() {
|
|
10232
|
+
try {
|
|
10233
|
+
peersNow = loadPeers(opts.peersFile);
|
|
10234
|
+
} catch (error) {
|
|
10235
|
+
limited("outbox.peers-invalid", { code: safeCode(error) });
|
|
10236
|
+
}
|
|
10237
|
+
return peersNow;
|
|
10238
|
+
}
|
|
10239
|
+
async function processRequest(frame) {
|
|
10240
|
+
const itemId = parseRequest(frame);
|
|
10241
|
+
if (!admitRequest())
|
|
10242
|
+
throw new Refusal("busy");
|
|
10243
|
+
if (lstatOrUndefined(opts.outboxRoot)?.isDirectory() !== true)
|
|
10244
|
+
throw new Refusal("root-unsafe");
|
|
10245
|
+
const found = findItem(opts.outboxRoot, itemId);
|
|
10246
|
+
const raw = readItemFile(found.file);
|
|
10247
|
+
const item = parseItem(raw, itemId, found.peer);
|
|
10248
|
+
const digest = itemHash(item);
|
|
10249
|
+
if (digest !== item.approvedSha256)
|
|
10250
|
+
throw new Refusal("hash-mismatch");
|
|
10251
|
+
const approved = readLedger(opts.outboxRoot, itemId);
|
|
10252
|
+
if (approved === void 0)
|
|
10253
|
+
throw new Refusal("not-approved");
|
|
10254
|
+
if (approved !== digest)
|
|
10255
|
+
throw new Refusal("hash-mismatch");
|
|
10256
|
+
const profile = loadNow();
|
|
10257
|
+
const peers = currentPeers();
|
|
10258
|
+
const peer = Object.hasOwn(profile.peers, item.to) ? profile.peers[item.to] : void 0;
|
|
10259
|
+
if (peer?.status !== "paired")
|
|
10260
|
+
throw new Refusal("unpaired-peer");
|
|
10261
|
+
if (!textOk(item.text, item.subject))
|
|
10262
|
+
throw new Refusal("invalid-text");
|
|
10263
|
+
if (recordedState(opts.sentDir, item.itemId) === "uncertain")
|
|
10264
|
+
throw new Refusal("uncertain");
|
|
10265
|
+
const outPerHour = peers === void 0 ? UNLOADED_OUT_PER_HOUR : policyOf(peers, item.to).outPerHour;
|
|
10266
|
+
checkRate(opts.sentDir, item.to, outPerHour, now());
|
|
10267
|
+
if (dryRun)
|
|
10268
|
+
throw new Refusal("dry-run");
|
|
10269
|
+
const marker = `${JSON.stringify({ v: 1, peer: item.to, at: new Date(now()).toISOString() })}
|
|
10270
|
+
`;
|
|
10271
|
+
if (!writeNew(opts.sentDir, item.itemId, marker, 384))
|
|
10272
|
+
throw new Refusal("already-used");
|
|
10273
|
+
const input = {
|
|
10274
|
+
which: item.thread === null ? "new" : "thread",
|
|
10275
|
+
threadId: item.thread ?? ulid(),
|
|
10276
|
+
subject: item.subject,
|
|
10277
|
+
text: item.text,
|
|
10278
|
+
intent: "request",
|
|
10279
|
+
asks: [],
|
|
10280
|
+
constraints: [],
|
|
10281
|
+
inReplyTo: null,
|
|
10282
|
+
referenceTaskIds: []
|
|
10283
|
+
};
|
|
10284
|
+
let sent;
|
|
10285
|
+
try {
|
|
10286
|
+
sent = await send(profile, item.to, input);
|
|
10287
|
+
} catch (error) {
|
|
10288
|
+
recordResult(opts.sentDir, item.itemId, "failed", void 0, now(), limited);
|
|
10289
|
+
limited("outbox.send-failed", { code: safeCode(error) });
|
|
10290
|
+
throw new Refusal("send-failed");
|
|
10291
|
+
}
|
|
10292
|
+
recordResult(opts.sentDir, item.itemId, "sent", sent.messageId, now(), limited);
|
|
10293
|
+
}
|
|
10294
|
+
function refusal(code) {
|
|
10295
|
+
limited("outbox.refused", { code });
|
|
10296
|
+
return `refused:${code}`;
|
|
10297
|
+
}
|
|
10298
|
+
async function serveRequest(frame) {
|
|
10299
|
+
try {
|
|
10300
|
+
await processRequest(frame);
|
|
10301
|
+
return "ok";
|
|
10302
|
+
} catch (error) {
|
|
10303
|
+
if (error instanceof Refusal)
|
|
10304
|
+
return refusal(error.code);
|
|
10305
|
+
limited("outbox.internal", { code: safeCode(error) });
|
|
10306
|
+
return "refused:internal";
|
|
10307
|
+
}
|
|
10308
|
+
}
|
|
10309
|
+
function respond(socket, reply) {
|
|
10310
|
+
if (socket.destroyed)
|
|
10311
|
+
return;
|
|
10312
|
+
socket.end(`${reply}
|
|
10313
|
+
`);
|
|
10314
|
+
socket.setTimeout(PIPE_TIMEOUT_MS, () => socket.destroy());
|
|
10315
|
+
}
|
|
10316
|
+
function enqueue(socket, frame, sawExtraBytes) {
|
|
10317
|
+
if (frame === void 0) {
|
|
10318
|
+
respond(socket, refusal("bad-request"));
|
|
10319
|
+
return;
|
|
10320
|
+
}
|
|
10321
|
+
if (pending >= MAX_PENDING) {
|
|
10322
|
+
respond(socket, refusal("busy"));
|
|
10323
|
+
return;
|
|
10324
|
+
}
|
|
10325
|
+
pending++;
|
|
10326
|
+
chain = chain.then(async () => {
|
|
10327
|
+
pending--;
|
|
10328
|
+
respond(socket, sawExtraBytes() ? refusal("bad-request") : await serveRequest(frame));
|
|
10329
|
+
});
|
|
10330
|
+
}
|
|
10331
|
+
function accept(socket) {
|
|
10332
|
+
if (open >= MAX_CONNECTIONS) {
|
|
10333
|
+
socket.destroy();
|
|
10334
|
+
return;
|
|
10335
|
+
}
|
|
10336
|
+
open++;
|
|
10337
|
+
socket.on("error", (error) => limited("outbox.socket-error", { code: safeCode(error) }));
|
|
10338
|
+
socket.on("close", () => {
|
|
10339
|
+
open--;
|
|
10340
|
+
if (once)
|
|
10341
|
+
void close();
|
|
10342
|
+
});
|
|
10343
|
+
let received = Buffer.alloc(0);
|
|
10344
|
+
let decided = false;
|
|
10345
|
+
let extraBytes = false;
|
|
10346
|
+
const decide2 = (frame) => {
|
|
10347
|
+
decided = true;
|
|
10348
|
+
enqueue(socket, frame, () => extraBytes);
|
|
10349
|
+
};
|
|
10350
|
+
socket.setTimeout(PIPE_TIMEOUT_MS, () => {
|
|
10351
|
+
if (!decided && received.length > 0)
|
|
10352
|
+
decide2(void 0);
|
|
10353
|
+
else
|
|
10354
|
+
socket.destroy();
|
|
10355
|
+
});
|
|
10356
|
+
const onData = (chunk) => {
|
|
10357
|
+
if (decided) {
|
|
10358
|
+
extraBytes = true;
|
|
10359
|
+
return;
|
|
10360
|
+
}
|
|
10361
|
+
if (received.length + chunk.length > MAX_REQUEST_BYTES + 1) {
|
|
10362
|
+
decide2(void 0);
|
|
10363
|
+
return;
|
|
10364
|
+
}
|
|
10365
|
+
received = Buffer.concat([received, chunk]);
|
|
10366
|
+
const newline = received.indexOf(10);
|
|
10367
|
+
if (newline === -1)
|
|
10368
|
+
return;
|
|
10369
|
+
decide2(newline === received.length - 1 ? received.subarray(0, newline) : void 0);
|
|
10370
|
+
};
|
|
10371
|
+
const onEnd = () => {
|
|
10372
|
+
if (decided)
|
|
10373
|
+
return;
|
|
10374
|
+
if (received.length === 0)
|
|
10375
|
+
socket.destroy();
|
|
10376
|
+
else
|
|
10377
|
+
decide2(received);
|
|
10378
|
+
};
|
|
10379
|
+
socket.on("data", onData);
|
|
10380
|
+
socket.on("end", onEnd);
|
|
10381
|
+
}
|
|
10382
|
+
return { close, done };
|
|
10383
|
+
}
|
|
10384
|
+
function configValue(args, flag, env, usage2, fallback) {
|
|
10385
|
+
const value2 = args.get(flag) ?? process.env[env] ?? fallback;
|
|
10386
|
+
if (value2 === void 0 || value2 === "true" || value2 === "") {
|
|
10387
|
+
throw new CourierError("usage", `--${flag} (or ${env}) must be set.
|
|
10388
|
+
${usage2}`);
|
|
10389
|
+
}
|
|
10390
|
+
return value2;
|
|
10391
|
+
}
|
|
10392
|
+
async function cmdOutboxServe(args) {
|
|
10393
|
+
const base = profileDir();
|
|
10394
|
+
const handle = await startOutbox({
|
|
10395
|
+
outboxRoot: configValue(args, "outbox-dir", "COURIER_OUTBOX_DIR", SERVE_USAGE),
|
|
10396
|
+
pipe: configValue(args, "pipe", "COURIER_OUTBOX_PIPE", SERVE_USAGE),
|
|
10397
|
+
socketGroup: configValue(args, "socket-group", "COURIER_OUTBOX_SOCKET_GROUP", SERVE_USAGE, DEFAULT_SOCKET_GROUP),
|
|
10398
|
+
peersFile: args.get("peers-file") ?? path19.join(base, "intake", "peers.json"),
|
|
10399
|
+
sentDir: args.get("sent-dir") ?? path19.join(base, "outbox-sent"),
|
|
10400
|
+
once: args.has("once"),
|
|
10401
|
+
dryRun: args.has("dry-run")
|
|
10402
|
+
});
|
|
10403
|
+
const stop = () => {
|
|
10404
|
+
void handle.close();
|
|
10405
|
+
};
|
|
10406
|
+
process.once("SIGINT", stop);
|
|
10407
|
+
process.once("SIGTERM", stop);
|
|
10408
|
+
await handle.done;
|
|
10409
|
+
}
|
|
10410
|
+
function ensurePeerDir(root, peerId) {
|
|
10411
|
+
const peerDir = path19.join(root, peerId);
|
|
10412
|
+
const existing = lstatOrUndefined(peerDir);
|
|
10413
|
+
if (existing === void 0) {
|
|
10414
|
+
try {
|
|
10415
|
+
fs17.accessSync(root, fs17.constants.W_OK);
|
|
10416
|
+
} catch {
|
|
10417
|
+
throw new CourierError("outbox-not-writable", "outbox root is not writable by this user");
|
|
10418
|
+
}
|
|
10419
|
+
fs17.mkdirSync(peerDir, 1512);
|
|
10420
|
+
fs17.chmodSync(peerDir, 1512);
|
|
10421
|
+
fsyncDir(root);
|
|
10422
|
+
} else if (!existing.isDirectory()) {
|
|
10423
|
+
throw new CourierError("root-unsafe", "peer outbox path is not a real directory");
|
|
10424
|
+
}
|
|
10425
|
+
return peerDir;
|
|
10426
|
+
}
|
|
10427
|
+
function publishItem(dir, itemId, body) {
|
|
10428
|
+
const temp = path19.join(dir, `.tmp-${randomBytes4(9).toString("hex")}`);
|
|
10429
|
+
const fd = fs17.openSync(temp, "wx", ITEM_MODE);
|
|
10430
|
+
try {
|
|
10431
|
+
fs17.writeFileSync(fd, body);
|
|
10432
|
+
fs17.fchmodSync(fd, ITEM_MODE);
|
|
10433
|
+
fs17.fsyncSync(fd);
|
|
10434
|
+
} catch (error) {
|
|
10435
|
+
fs17.closeSync(fd);
|
|
10436
|
+
fs17.rmSync(temp, { force: true });
|
|
10437
|
+
throw error;
|
|
10438
|
+
}
|
|
10439
|
+
fs17.closeSync(fd);
|
|
10440
|
+
try {
|
|
10441
|
+
fs17.linkSync(temp, path19.join(dir, `${itemId}.json`));
|
|
10442
|
+
fsyncDir(dir);
|
|
10443
|
+
return true;
|
|
10444
|
+
} catch (error) {
|
|
10445
|
+
if (error.code === "EEXIST")
|
|
10446
|
+
return false;
|
|
10447
|
+
throw error;
|
|
10448
|
+
} finally {
|
|
10449
|
+
fs17.rmSync(temp, { force: true });
|
|
10450
|
+
}
|
|
10451
|
+
}
|
|
10452
|
+
function ensureLedgerDir(root) {
|
|
10453
|
+
const dir = path19.join(root, APPROVED_DIR);
|
|
10454
|
+
if (ensureRealDir(dir, 448))
|
|
10455
|
+
fsyncDir(root);
|
|
10456
|
+
return dir;
|
|
10457
|
+
}
|
|
10458
|
+
function cmdOutboxAdd(args) {
|
|
10459
|
+
const peerId = args.positional[0];
|
|
10460
|
+
if (peerId === void 0 || !PEER_RE.test(peerId) || peerId === APPROVED_DIR)
|
|
10461
|
+
throw new CourierError("usage", ADD_USAGE);
|
|
10462
|
+
const which = oneOf(args, ["thread", "new"]);
|
|
10463
|
+
const thread = which === "thread" ? requireFlag(args, "thread", ADD_USAGE) : null;
|
|
10464
|
+
if (thread !== null && !ULID_RE2.test(thread))
|
|
10465
|
+
throw new CourierError("usage", ADD_USAGE);
|
|
10466
|
+
const subject = which === "new" ? requireFlag(args, "new", ADD_USAGE) : void 0;
|
|
10467
|
+
const text = readTextArg(args, ADD_USAGE);
|
|
10468
|
+
if (!textOk(text, subject)) {
|
|
10469
|
+
throw new CourierError("invalid-text", "text must be 1..2000 characters; subject at most 200 characters");
|
|
10470
|
+
}
|
|
10471
|
+
const peer = getPeer(loadProfile(), peerId);
|
|
10472
|
+
if (peer.status !== "paired")
|
|
10473
|
+
throw new CourierError("unpaired-peer", `peer ${peerId} is not paired`);
|
|
10474
|
+
const root = configValue(args, "outbox-dir", "COURIER_OUTBOX_DIR", ADD_USAGE);
|
|
10475
|
+
if (lstatOrUndefined(root)?.isDirectory() !== true) {
|
|
10476
|
+
throw new CourierError("root-unsafe", "outbox root must be an existing real directory");
|
|
10477
|
+
}
|
|
10478
|
+
const itemId = ulid();
|
|
10479
|
+
const body = { v: 1, itemId, to: peerId, thread, ...subject === void 0 ? {} : { subject }, text };
|
|
10480
|
+
const approvedSha256 = itemHash(body);
|
|
10481
|
+
const item = { ...body, approvedSha256 };
|
|
10482
|
+
const peerDir = ensurePeerDir(root, peerId);
|
|
10483
|
+
if (!writeNew(ensureLedgerDir(root), itemId, `${approvedSha256}
|
|
10484
|
+
`, ITEM_MODE)) {
|
|
10485
|
+
throw new CourierError("exists", "item id collision; retry");
|
|
10486
|
+
}
|
|
10487
|
+
if (!publishItem(peerDir, itemId, `${canonicalJson(item)}
|
|
10488
|
+
`)) {
|
|
10489
|
+
throw new CourierError("exists", "item id collision; retry");
|
|
10490
|
+
}
|
|
10491
|
+
process.stdout.write(`${JSON.stringify({ ok: true, itemId, peer: peerId }, null, 2)}
|
|
10492
|
+
`);
|
|
10493
|
+
}
|
|
10494
|
+
async function cmdOutbox(args) {
|
|
10495
|
+
const action = args.positional[0];
|
|
10496
|
+
const rest = { ...args, positional: args.positional.slice(1) };
|
|
10497
|
+
if (action === "add")
|
|
10498
|
+
return cmdOutboxAdd(rest);
|
|
10499
|
+
if (action === "serve")
|
|
10500
|
+
return cmdOutboxServe(rest);
|
|
10501
|
+
throw new CourierError("usage", OUTBOX_USAGE);
|
|
10502
|
+
}
|
|
10503
|
+
|
|
9855
10504
|
// ../courier/dist/consumed.js
|
|
9856
10505
|
function isKeyMissing(error) {
|
|
9857
10506
|
const status = String(error.status ?? error.meta?.status ?? "");
|
|
@@ -10039,9 +10688,9 @@ ${targetHelp()}`);
|
|
|
10039
10688
|
|
|
10040
10689
|
// ../courier/dist/broker.js
|
|
10041
10690
|
import { execFile } from "child_process";
|
|
10042
|
-
import
|
|
10691
|
+
import fs18 from "fs";
|
|
10043
10692
|
import os8 from "os";
|
|
10044
|
-
import
|
|
10693
|
+
import path20 from "path";
|
|
10045
10694
|
var MAX_OUTPUT = 4e3;
|
|
10046
10695
|
var EXEC_TIMEOUT_MS = 12e4;
|
|
10047
10696
|
var TOKEN = /^(gh[pousr]_[A-Za-z0-9]{20,255}|github_pat_[A-Za-z0-9_]{20,255})$/;
|
|
@@ -10053,7 +10702,7 @@ async function loadBrokerCredential(config, exec = execFileRunner) {
|
|
|
10053
10702
|
const result = await exec(config.ghBin, ["auth", "token", "--hostname", "github.com"], {
|
|
10054
10703
|
cwd: ownerHome,
|
|
10055
10704
|
timeoutMs: 1e4,
|
|
10056
|
-
env: { PATH: "/usr/bin:/bin", HOME: ownerHome, GH_CONFIG_DIR:
|
|
10705
|
+
env: { PATH: "/usr/bin:/bin", HOME: ownerHome, GH_CONFIG_DIR: path20.join(ownerHome, ".config/gh"), GH_PROMPT_DISABLED: "1", GH_NO_UPDATE_NOTIFIER: "1", LANG: "C" }
|
|
10057
10706
|
});
|
|
10058
10707
|
const token2 = result.stdout.trim();
|
|
10059
10708
|
if (result.code !== 0 || !validToken(token2))
|
|
@@ -10103,9 +10752,9 @@ var execFileRunner = (bin, args, { cwd, env, timeoutMs }) => {
|
|
|
10103
10752
|
};
|
|
10104
10753
|
function brokerEnv(credential, scratch) {
|
|
10105
10754
|
return {
|
|
10106
|
-
PATH: `${
|
|
10755
|
+
PATH: `${path20.dirname(credential.ghBin)}:/usr/bin:/bin`,
|
|
10107
10756
|
HOME: scratch,
|
|
10108
|
-
GH_CONFIG_DIR:
|
|
10757
|
+
GH_CONFIG_DIR: path20.join(scratch, "gh"),
|
|
10109
10758
|
GH_TOKEN: credential.token,
|
|
10110
10759
|
GH_PROMPT_DISABLED: "1",
|
|
10111
10760
|
GH_NO_UPDATE_NOTIFIER: "1",
|
|
@@ -10137,8 +10786,8 @@ async function runApproved(id, deps) {
|
|
|
10137
10786
|
if (await isConsumed(deps.kv, id))
|
|
10138
10787
|
fail4("approval-used", `Approval ${id} was already used; an approval covers one action.`);
|
|
10139
10788
|
const scratchRoot = deps.scratchRoot ?? brokerRoot();
|
|
10140
|
-
|
|
10141
|
-
const scratch =
|
|
10789
|
+
fs18.mkdirSync(scratchRoot, { recursive: true, mode: 448 });
|
|
10790
|
+
const scratch = fs18.mkdtempSync(path20.join(scratchRoot, "run-"));
|
|
10142
10791
|
const env = brokerEnv(credential, scratch);
|
|
10143
10792
|
const run2 = (args) => deps.exec(credential.ghBin, args, { cwd: scratch, env, timeoutMs: EXEC_TIMEOUT_MS });
|
|
10144
10793
|
const clean = (text) => sanitizeOutput(text, [credential.token]);
|
|
@@ -10175,7 +10824,7 @@ ${merged.stderr}` : ""}`).trim(),
|
|
|
10175
10824
|
...ok2 ? {} : { note: "The approval is used up. If the merge should still happen, file a new request." }
|
|
10176
10825
|
};
|
|
10177
10826
|
} finally {
|
|
10178
|
-
|
|
10827
|
+
fs18.rmSync(scratch, { recursive: true, force: true });
|
|
10179
10828
|
}
|
|
10180
10829
|
}
|
|
10181
10830
|
var runApprovedUsage = () => `Usage: ${cmd("run-approved <requestId>")}`;
|
|
@@ -10187,7 +10836,7 @@ async function cmdRunApproved(args) {
|
|
|
10187
10836
|
const config = loadBrokerConfig(currentProfileId(), testOwnerRoot);
|
|
10188
10837
|
assertPinnedProfile(config, loadProfile());
|
|
10189
10838
|
const node = await courierNode(loadProfile(config.courierHome), config.courierHome);
|
|
10190
|
-
const result = await runApproved(id, { kv: node.kv, credential: () => loadBrokerCredential(config), scratchRoot:
|
|
10839
|
+
const result = await runApproved(id, { kv: node.kv, credential: () => loadBrokerCredential(config), scratchRoot: path20.dirname(brokerFile(currentProfileId(), testOwnerRoot)), exec: execFileRunner, now: () => Date.now() });
|
|
10191
10840
|
await writeOut(`${JSON.stringify(result, null, 2)}
|
|
10192
10841
|
`);
|
|
10193
10842
|
if (!result.ok)
|
|
@@ -10215,7 +10864,7 @@ async function cmdBroker(args) {
|
|
|
10215
10864
|
return;
|
|
10216
10865
|
}
|
|
10217
10866
|
if (sub2 === "clear") {
|
|
10218
|
-
|
|
10867
|
+
fs18.rmSync(file, { force: true });
|
|
10219
10868
|
await writeOut('{"configured":false}\n');
|
|
10220
10869
|
return;
|
|
10221
10870
|
}
|
|
@@ -10225,33 +10874,33 @@ async function cmdBroker(args) {
|
|
|
10225
10874
|
if (ghBin === "true")
|
|
10226
10875
|
fail4("usage", "--gh-bin needs a path.");
|
|
10227
10876
|
if (ghBin === void 0) {
|
|
10228
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
10229
|
-
const candidate =
|
|
10877
|
+
for (const dir of (process.env.PATH ?? "").split(path20.delimiter)) {
|
|
10878
|
+
const candidate = path20.join(dir || ".", "gh");
|
|
10230
10879
|
try {
|
|
10231
|
-
|
|
10232
|
-
ghBin =
|
|
10880
|
+
fs18.accessSync(candidate, fs18.constants.X_OK);
|
|
10881
|
+
ghBin = fs18.realpathSync(candidate);
|
|
10233
10882
|
break;
|
|
10234
10883
|
} catch {
|
|
10235
10884
|
}
|
|
10236
10885
|
}
|
|
10237
10886
|
}
|
|
10238
|
-
if (!ghBin || !
|
|
10887
|
+
if (!ghBin || !path20.isAbsolute(ghBin))
|
|
10239
10888
|
fail4("usage", "Could not find gh; pass --gh-bin <absolute path>.");
|
|
10240
10889
|
try {
|
|
10241
|
-
|
|
10890
|
+
fs18.accessSync(ghBin, fs18.constants.X_OK);
|
|
10242
10891
|
} catch {
|
|
10243
10892
|
fail4("usage", `${ghBin} is not an executable file.`);
|
|
10244
10893
|
}
|
|
10245
10894
|
const profile = loadProfile();
|
|
10246
10895
|
let testToken;
|
|
10247
10896
|
if (testOwnerRoot !== void 0) {
|
|
10248
|
-
testToken =
|
|
10897
|
+
testToken = fs18.readFileSync(0, "utf8").trim();
|
|
10249
10898
|
if (testToken !== TEST_TOKEN)
|
|
10250
10899
|
fail4("usage", "Tests accept only the fixed dummy token fixture on stdin");
|
|
10251
10900
|
}
|
|
10252
|
-
ghBin =
|
|
10253
|
-
const st =
|
|
10254
|
-
const config = { v: 2, courierHome:
|
|
10901
|
+
ghBin = fs18.realpathSync(ghBin);
|
|
10902
|
+
const st = fs18.statSync(ghBin);
|
|
10903
|
+
const config = { v: 2, courierHome: fs18.realpathSync(profileDir()), profile: pinProfile(profile), ghBin, ghIdentity: { dev: st.dev, ino: st.ino }, ...testToken ? { testToken } : {} };
|
|
10255
10904
|
if (!testToken)
|
|
10256
10905
|
await loadBrokerCredential(config);
|
|
10257
10906
|
saveBrokerConfig(config, testOwnerRoot);
|
|
@@ -10349,6 +10998,13 @@ Delivery (one watcher per person per machine; never calls a model):
|
|
|
10349
10998
|
intake [--queue-dir d] [--peers-file f] [--state-dir d] [--interval 60] [--once]
|
|
10350
10999
|
Intake reader: copy paired peers' mirrored messages into the local queue
|
|
10351
11000
|
(one <msgId>.json per message) with cheap checks. Never calls a model.
|
|
11001
|
+
outbox add <peer> (--thread <id> | --new <subject>) [--text t | --file f | stdin] [--outbox-dir d]
|
|
11002
|
+
Queue one message for a paired peer as a single-use outbox item. Text limit:
|
|
11003
|
+
2000 characters (8 KiB UTF-8); subject limit: 200 characters.
|
|
11004
|
+
outbox serve --pipe <socket> [--outbox-dir d] [--socket-group courier-sender] [--peers-file f]
|
|
11005
|
+
[--sent-dir d] [--once] [--dry-run]
|
|
11006
|
+
Sender: accept one-line item requests over a Unix socket and send the
|
|
11007
|
+
named outbox item. Replies are ok or refused:<code>.
|
|
10352
11008
|
|
|
10353
11009
|
pause <peer> <threadId> [--reason "\u2026"] Humans: stop waking your agent for a thread; tells the peer
|
|
10354
11010
|
resume <peer> <threadId> Humans: wake it again (the wake budget starts over)
|
|
@@ -10399,6 +11055,8 @@ async function run(argv) {
|
|
|
10399
11055
|
return cmdWatch(sub2);
|
|
10400
11056
|
case "intake":
|
|
10401
11057
|
return cmdIntake(sub2);
|
|
11058
|
+
case "outbox":
|
|
11059
|
+
return cmdOutbox(sub2);
|
|
10402
11060
|
case "approval":
|
|
10403
11061
|
return cmdApproval(sub2);
|
|
10404
11062
|
case "pause":
|