@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.
@@ -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 = (path20, actions) => ({
7
+ var kv = (path21, actions) => ({
8
8
  service: "tinycloud.kv",
9
9
  space: COURIER_SPACE,
10
- path: `${PREFIX}/${path20}`,
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: path20, action }) => `${service} ${space} ${path20 || '""'} ${action}`);
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 cmdSend(args) {
6548
- const { profile, node } = await ctx();
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: intentRaw,
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 path20 of keys) {
7388
- const match = /^([^/]+)\/(?:meta\.json|(?:msg|task)\/[^/]+\.json)$/.exec(path20.slice(prefix.length));
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(path20, ownerUid = process.getuid?.()) {
9057
+ function loadPeers(path21, ownerUid2 = process.getuid?.()) {
9047
9058
  let fd;
9048
9059
  try {
9049
- fd = fs14.openSync(path20, fs14.constants.O_RDONLY | fs14.constants.O_NOFOLLOW);
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", `${path20} is a symbolic link; point the flag at the file itself.`);
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", `${path20} is not a regular file.`);
9062
- if (ownerUid !== void 0 && stat.uid !== ownerUid)
9063
- throw new CourierError("insecure-peers", `${path20} is not owned by the courier user.`);
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", `${path20} is writable by group or others; restrict it to the owner (chmod 600).`);
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", `${path20} must be a JSON object keyed by peer id.`);
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", `${path20} has an invalid policy for peer "${peerId}"; fix the entry. Nothing was loaded.`);
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 fs17 from "fs";
10691
+ import fs18 from "fs";
10043
10692
  import os8 from "os";
10044
- import path19 from "path";
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: path19.join(ownerHome, ".config/gh"), GH_PROMPT_DISABLED: "1", GH_NO_UPDATE_NOTIFIER: "1", LANG: "C" }
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: `${path19.dirname(credential.ghBin)}:/usr/bin:/bin`,
10755
+ PATH: `${path20.dirname(credential.ghBin)}:/usr/bin:/bin`,
10107
10756
  HOME: scratch,
10108
- GH_CONFIG_DIR: path19.join(scratch, "gh"),
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
- fs17.mkdirSync(scratchRoot, { recursive: true, mode: 448 });
10141
- const scratch = fs17.mkdtempSync(path19.join(scratchRoot, "run-"));
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
- fs17.rmSync(scratch, { recursive: true, force: true });
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: path19.dirname(brokerFile(currentProfileId(), testOwnerRoot)), exec: execFileRunner, now: () => Date.now() });
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
- fs17.rmSync(file, { force: true });
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(path19.delimiter)) {
10229
- const candidate = path19.join(dir || ".", "gh");
10877
+ for (const dir of (process.env.PATH ?? "").split(path20.delimiter)) {
10878
+ const candidate = path20.join(dir || ".", "gh");
10230
10879
  try {
10231
- fs17.accessSync(candidate, fs17.constants.X_OK);
10232
- ghBin = fs17.realpathSync(candidate);
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 || !path19.isAbsolute(ghBin))
10887
+ if (!ghBin || !path20.isAbsolute(ghBin))
10239
10888
  fail4("usage", "Could not find gh; pass --gh-bin <absolute path>.");
10240
10889
  try {
10241
- fs17.accessSync(ghBin, fs17.constants.X_OK);
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 = fs17.readFileSync(0, "utf8").trim();
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 = fs17.realpathSync(ghBin);
10253
- const st = fs17.statSync(ghBin);
10254
- const config = { v: 2, courierHome: fs17.realpathSync(profileDir()), profile: pinProfile(profile), ghBin, ghIdentity: { dev: st.dev, ino: st.ino }, ...testToken ? { testToken } : {} };
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":