@tinycloud/cli 1.3.0-beta.48 → 1.3.0-beta.49
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 +919 -38
- package/dist/courier-entry.js.map +1 -1
- package/package.json +5 -5
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 = (path20, actions) => ({
|
|
8
8
|
service: "tinycloud.kv",
|
|
9
9
|
space: COURIER_SPACE,
|
|
10
|
-
path: `${PREFIX}/${
|
|
10
|
+
path: `${PREFIX}/${path20}`,
|
|
11
11
|
actions: actions.map((action) => `tinycloud.kv/${action}`)
|
|
12
12
|
});
|
|
13
13
|
var AGENT_GRANT = [
|
|
@@ -416,6 +416,30 @@ function errInfo(e) {
|
|
|
416
416
|
message: String(anyE.message ?? e).slice(0, 400)
|
|
417
417
|
};
|
|
418
418
|
}
|
|
419
|
+
var TRANSIENT_CODES = ["NETWORK_ERROR", "ECONNRESET", "ETIMEDOUT"];
|
|
420
|
+
function fieldOf(source, name2) {
|
|
421
|
+
return typeof source === "object" && source !== null ? Reflect.get(source, name2) : void 0;
|
|
422
|
+
}
|
|
423
|
+
function asStatus(value2) {
|
|
424
|
+
const n = typeof value2 === "string" && /^\d{3}$/.test(value2) ? Number(value2) : value2;
|
|
425
|
+
return typeof n === "number" && Number.isInteger(n) ? n : void 0;
|
|
426
|
+
}
|
|
427
|
+
function httpStatuses(error) {
|
|
428
|
+
return [fieldOf(error, "status"), fieldOf(fieldOf(error, "meta"), "status"), fieldOf(error, "code")].map(asStatus).filter((s) => s !== void 0);
|
|
429
|
+
}
|
|
430
|
+
function isPreconditionError(error) {
|
|
431
|
+
return fieldOf(error, "code") === "KV_PRECONDITION_FAILED" || httpStatuses(error).includes(412);
|
|
432
|
+
}
|
|
433
|
+
function isNotFoundError(error) {
|
|
434
|
+
return fieldOf(error, "code") === "KV_NOT_FOUND" || httpStatuses(error).includes(404);
|
|
435
|
+
}
|
|
436
|
+
function isTransientError(error) {
|
|
437
|
+
if (httpStatuses(error).some((s) => s >= 500 && s <= 599))
|
|
438
|
+
return true;
|
|
439
|
+
const code = fieldOf(error, "code");
|
|
440
|
+
const causeCode = fieldOf(fieldOf(error, "cause"), "code");
|
|
441
|
+
return TRANSIENT_CODES.some((c) => c === code || c === causeCode);
|
|
442
|
+
}
|
|
419
443
|
function unwrap(r, what) {
|
|
420
444
|
if (r && typeof r === "object" && "ok" in r) {
|
|
421
445
|
const res = r;
|
|
@@ -569,7 +593,6 @@ function defer() {
|
|
|
569
593
|
|
|
570
594
|
// ../courier/dist/store.js
|
|
571
595
|
var FULL = { prefix: "" };
|
|
572
|
-
var TRANSIENT = /fetch failed|NETWORK_ERROR|ECONNRESET|ETIMEDOUT|5\d\d/i;
|
|
573
596
|
async function nap(ms2) {
|
|
574
597
|
const { promise, resolve } = defer();
|
|
575
598
|
setTimeout(resolve, ms2);
|
|
@@ -582,8 +605,7 @@ async function readRetry(fn) {
|
|
|
582
605
|
const r = await fn();
|
|
583
606
|
const res = r;
|
|
584
607
|
if (res && typeof res === "object" && res.ok === false) {
|
|
585
|
-
|
|
586
|
-
if (TRANSIENT.test(m) && i < 2) {
|
|
608
|
+
if (isTransientError(res.error) && i < 2) {
|
|
587
609
|
await nap(1500);
|
|
588
610
|
continue;
|
|
589
611
|
}
|
|
@@ -591,7 +613,8 @@ async function readRetry(fn) {
|
|
|
591
613
|
return r;
|
|
592
614
|
} catch (e) {
|
|
593
615
|
last = e;
|
|
594
|
-
|
|
616
|
+
const undiciFetchFailure = e instanceof Error && e.message === "fetch failed";
|
|
617
|
+
if (!(isTransientError(e) || undiciFetchFailure) || i === 2)
|
|
595
618
|
throw e;
|
|
596
619
|
await nap(1500);
|
|
597
620
|
}
|
|
@@ -607,7 +630,7 @@ async function putCreate(kv2, key, value2) {
|
|
|
607
630
|
try {
|
|
608
631
|
return putEtag(unwrap(r, `put ${key}`));
|
|
609
632
|
} catch (e) {
|
|
610
|
-
if (e instanceof CourierError &&
|
|
633
|
+
if (e instanceof CourierError && r.ok === false && isPreconditionError(r.error)) {
|
|
611
634
|
throw new CourierError("conflict", `${key} already exists (create-only write)`);
|
|
612
635
|
}
|
|
613
636
|
throw e;
|
|
@@ -618,7 +641,7 @@ async function putMatch(kv2, key, value2, etag) {
|
|
|
618
641
|
try {
|
|
619
642
|
return putEtag(unwrap(r, `put ${key}`));
|
|
620
643
|
} catch (e) {
|
|
621
|
-
if (e instanceof CourierError &&
|
|
644
|
+
if (e instanceof CourierError && r.ok === false && isPreconditionError(r.error)) {
|
|
622
645
|
throw new CourierError("conflict", `${key} changed under us; re-read and retry`);
|
|
623
646
|
}
|
|
624
647
|
throw e;
|
|
@@ -627,11 +650,10 @@ async function putMatch(kv2, key, value2, etag) {
|
|
|
627
650
|
async function getJson(kv2, key) {
|
|
628
651
|
const r = await readRetry(() => kv2.get(key, FULL));
|
|
629
652
|
if (r && typeof r === "object" && "ok" in r && !r.ok) {
|
|
630
|
-
const status = r.error?.status ?? r.error?.meta?.status ?? r.error?.code;
|
|
631
653
|
if (/space not found/i.test(String(r.error?.message))) {
|
|
632
654
|
throw new CourierError("no-space", String(r.error?.message));
|
|
633
655
|
}
|
|
634
|
-
if (
|
|
656
|
+
if (isNotFoundError(r.error))
|
|
635
657
|
return void 0;
|
|
636
658
|
unwrap(r, `get ${key}`);
|
|
637
659
|
}
|
|
@@ -747,7 +769,7 @@ async function commitReceipts(selfKv, pending) {
|
|
|
747
769
|
await putCreate(selfKv, key, receipts);
|
|
748
770
|
break;
|
|
749
771
|
} catch (error) {
|
|
750
|
-
const conflict =
|
|
772
|
+
const conflict = error instanceof CourierError && error.code === "conflict";
|
|
751
773
|
if (conflict && attempt < 2)
|
|
752
774
|
continue;
|
|
753
775
|
console.error(`warning: could not advance receipts for ${peer}: ${errInfo(error).message}`);
|
|
@@ -804,7 +826,7 @@ function authorityTuples(entries) {
|
|
|
804
826
|
return tuples;
|
|
805
827
|
}
|
|
806
828
|
function renderTuples(tuples) {
|
|
807
|
-
const shown = tuples.slice(0, 3).map(({ service, space, path:
|
|
829
|
+
const shown = tuples.slice(0, 3).map(({ service, space, path: path20, action }) => `${service} ${space} ${path20 || '""'} ${action}`);
|
|
808
830
|
if (tuples.length > 3)
|
|
809
831
|
shown.push(`(+${tuples.length - 3} more)`);
|
|
810
832
|
return shown.join("; ");
|
|
@@ -1943,13 +1965,13 @@ function contentReason(value2, field) {
|
|
|
1943
1965
|
return "not-nfc";
|
|
1944
1966
|
return void 0;
|
|
1945
1967
|
}
|
|
1946
|
-
function textVerdict(value2, field) {
|
|
1968
|
+
function textVerdict(value2, field, maxUnits) {
|
|
1947
1969
|
let reason;
|
|
1948
1970
|
if (typeof value2 !== "string")
|
|
1949
1971
|
reason = "not-string";
|
|
1950
1972
|
else if (value2.length === 0)
|
|
1951
1973
|
reason = "empty";
|
|
1952
|
-
else if (value2.length > (field === "multiline" ? MAX_ACTION : MAX_SUMMARY))
|
|
1974
|
+
else if (value2.length > (maxUnits ?? (field === "multiline" ? MAX_ACTION : MAX_SUMMARY)))
|
|
1953
1975
|
reason = "too-long";
|
|
1954
1976
|
else if (field !== "multiline" && (value2.startsWith(" ") || value2.endsWith(" ")))
|
|
1955
1977
|
reason = "edge-whitespace";
|
|
@@ -7362,8 +7384,8 @@ var Watcher = class {
|
|
|
7362
7384
|
const prefix = `${PREFIX}/to/${peer.handle}/threads/`;
|
|
7363
7385
|
const keys = await listAll(this.deps.selfKv, prefix);
|
|
7364
7386
|
const known = /* @__PURE__ */ new Set();
|
|
7365
|
-
for (const
|
|
7366
|
-
const match = /^([^/]+)\/(?:meta\.json|(?:msg|task)\/[^/]+\.json)$/.exec(
|
|
7387
|
+
for (const path20 of keys) {
|
|
7388
|
+
const match = /^([^/]+)\/(?:meta\.json|(?:msg|task)\/[^/]+\.json)$/.exec(path20.slice(prefix.length));
|
|
7367
7389
|
if (match && SAFE_ID.test(match[1]))
|
|
7368
7390
|
known.add(`${peer.handle}/${match[1]}`);
|
|
7369
7391
|
}
|
|
@@ -8976,6 +8998,860 @@ async function cmdWatch(args) {
|
|
|
8976
8998
|
}
|
|
8977
8999
|
}
|
|
8978
9000
|
|
|
9001
|
+
// ../courier/dist/intake/intake.js
|
|
9002
|
+
import fs16 from "fs";
|
|
9003
|
+
import path18 from "path";
|
|
9004
|
+
|
|
9005
|
+
// ../courier/dist/intake/flags.js
|
|
9006
|
+
var FLAG_TABLE = {
|
|
9007
|
+
"too-long": { severity: "tiered", description: "text is over 8000 UTF-16 units; the text is still queued" },
|
|
9008
|
+
link: { severity: "soft", description: "a URL-like string is present" },
|
|
9009
|
+
instruction: { severity: "soft", description: "matches the fixed instruction-phrase table" },
|
|
9010
|
+
request: { severity: "soft", description: "asks the reader or its principal to take an action (fixed verb-phrase table)" },
|
|
9011
|
+
"secret-request": { severity: "hard", description: "asks for keys, tokens, passwords or credentials" },
|
|
9012
|
+
encoded: { severity: "hard", description: "a long base64- or hex-looking run is present" },
|
|
9013
|
+
unicode: { severity: "hard", description: "shared text verdict: not-nfc, disallowed-code-point, edge-whitespace, not-string or empty" },
|
|
9014
|
+
size: { severity: "hard", description: "original text exceeded the 16 KiB queue cap and was cut at a code-point boundary" },
|
|
9015
|
+
rate: { severity: "hard", description: "peer is over its inbound per-hour cap" }
|
|
9016
|
+
};
|
|
9017
|
+
|
|
9018
|
+
// ../courier/dist/intake/peers.js
|
|
9019
|
+
import fs14 from "fs";
|
|
9020
|
+
var DEFAULT_IN_PER_HOUR = 100;
|
|
9021
|
+
var DEFAULT_OUT_PER_HOUR = 100;
|
|
9022
|
+
var KEYS = { tier: true, ratePerHour: true, outPerHour: true };
|
|
9023
|
+
var TIERS = { trusted: true, strict: true };
|
|
9024
|
+
var ABSENT = { tier: "strict", ratePerHour: DEFAULT_IN_PER_HOUR, outPerHour: DEFAULT_OUT_PER_HOUR };
|
|
9025
|
+
function cap(value2, fallback) {
|
|
9026
|
+
if (value2 === void 0)
|
|
9027
|
+
return fallback;
|
|
9028
|
+
if (!Number.isInteger(value2) || value2 < 1 || value2 > fallback)
|
|
9029
|
+
return void 0;
|
|
9030
|
+
return value2;
|
|
9031
|
+
}
|
|
9032
|
+
function parseEntry(value2) {
|
|
9033
|
+
if (!isRecord(value2))
|
|
9034
|
+
return void 0;
|
|
9035
|
+
if (Object.keys(value2).some((key) => !Object.hasOwn(KEYS, key)))
|
|
9036
|
+
return void 0;
|
|
9037
|
+
const tier = value2.tier ?? "strict";
|
|
9038
|
+
if (typeof tier !== "string" || !Object.hasOwn(TIERS, tier))
|
|
9039
|
+
return void 0;
|
|
9040
|
+
const ratePerHour = cap(value2.ratePerHour, DEFAULT_IN_PER_HOUR);
|
|
9041
|
+
const outPerHour = cap(value2.outPerHour, DEFAULT_OUT_PER_HOUR);
|
|
9042
|
+
if (ratePerHour === void 0 || outPerHour === void 0)
|
|
9043
|
+
return void 0;
|
|
9044
|
+
return { tier, ratePerHour, outPerHour };
|
|
9045
|
+
}
|
|
9046
|
+
function loadPeers(path20, ownerUid = process.getuid?.()) {
|
|
9047
|
+
let fd;
|
|
9048
|
+
try {
|
|
9049
|
+
fd = fs14.openSync(path20, fs14.constants.O_RDONLY | fs14.constants.O_NOFOLLOW);
|
|
9050
|
+
} catch (error) {
|
|
9051
|
+
const code = error.code;
|
|
9052
|
+
if (code === "ENOENT")
|
|
9053
|
+
return /* @__PURE__ */ Object.create(null);
|
|
9054
|
+
if (code === "ELOOP")
|
|
9055
|
+
throw new CourierError("insecure-peers", `${path20} is a symbolic link; point the flag at the file itself.`);
|
|
9056
|
+
throw error;
|
|
9057
|
+
}
|
|
9058
|
+
try {
|
|
9059
|
+
const stat = fs14.fstatSync(fd);
|
|
9060
|
+
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.`);
|
|
9064
|
+
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).`);
|
|
9066
|
+
}
|
|
9067
|
+
const parsed = JSON.parse(fs14.readFileSync(fd, "utf8"));
|
|
9068
|
+
if (!isRecord(parsed))
|
|
9069
|
+
throw new CourierError("bad-peers", `${path20} must be a JSON object keyed by peer id.`);
|
|
9070
|
+
const peers = /* @__PURE__ */ Object.create(null);
|
|
9071
|
+
for (const [peerId, value2] of Object.entries(parsed)) {
|
|
9072
|
+
const policy = parseEntry(value2);
|
|
9073
|
+
if (!policy)
|
|
9074
|
+
throw new CourierError("bad-peers", `${path20} has an invalid policy for peer "${peerId}"; fix the entry. Nothing was loaded.`);
|
|
9075
|
+
peers[peerId] = policy;
|
|
9076
|
+
}
|
|
9077
|
+
return peers;
|
|
9078
|
+
} finally {
|
|
9079
|
+
fs14.closeSync(fd);
|
|
9080
|
+
}
|
|
9081
|
+
}
|
|
9082
|
+
function policyOf(peers, peerId) {
|
|
9083
|
+
return Object.hasOwn(peers, peerId) ? peers[peerId] : { ...ABSENT };
|
|
9084
|
+
}
|
|
9085
|
+
|
|
9086
|
+
// ../courier/dist/intake/queue.js
|
|
9087
|
+
import fs15 from "fs";
|
|
9088
|
+
import path17 from "path";
|
|
9089
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
9090
|
+
function ensureRealDir(dir, mode) {
|
|
9091
|
+
let existed = true;
|
|
9092
|
+
try {
|
|
9093
|
+
fs15.lstatSync(dir);
|
|
9094
|
+
} catch (error) {
|
|
9095
|
+
if (error.code !== "ENOENT")
|
|
9096
|
+
throw error;
|
|
9097
|
+
existed = false;
|
|
9098
|
+
}
|
|
9099
|
+
fs15.mkdirSync(dir, { recursive: true, mode });
|
|
9100
|
+
const stat = fs15.lstatSync(dir);
|
|
9101
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
9102
|
+
throw new CourierError("unsafe-dir", "queue or state path is not a real directory");
|
|
9103
|
+
}
|
|
9104
|
+
if (!existed && stat.uid === process.getuid?.() && (stat.mode & 4095) !== mode)
|
|
9105
|
+
fs15.chmodSync(dir, mode);
|
|
9106
|
+
}
|
|
9107
|
+
function fsyncDir(dir) {
|
|
9108
|
+
const fd = fs15.openSync(dir, "r");
|
|
9109
|
+
try {
|
|
9110
|
+
fs15.fsyncSync(fd);
|
|
9111
|
+
} finally {
|
|
9112
|
+
fs15.closeSync(fd);
|
|
9113
|
+
}
|
|
9114
|
+
}
|
|
9115
|
+
function writeTemp(dir, name2, body, mode) {
|
|
9116
|
+
const temp = path17.join(dir, `.${name2}.${randomBytes3(6).toString("hex")}.tmp`);
|
|
9117
|
+
const fd = fs15.openSync(temp, "wx", mode);
|
|
9118
|
+
try {
|
|
9119
|
+
fs15.writeFileSync(fd, body);
|
|
9120
|
+
fs15.fchmodSync(fd, mode);
|
|
9121
|
+
fs15.fsyncSync(fd);
|
|
9122
|
+
} catch (error) {
|
|
9123
|
+
fs15.closeSync(fd);
|
|
9124
|
+
fs15.rmSync(temp, { force: true });
|
|
9125
|
+
throw error;
|
|
9126
|
+
}
|
|
9127
|
+
fs15.closeSync(fd);
|
|
9128
|
+
return temp;
|
|
9129
|
+
}
|
|
9130
|
+
function writeNew(dir, name2, body, mode) {
|
|
9131
|
+
const temp = writeTemp(dir, name2, body, mode);
|
|
9132
|
+
try {
|
|
9133
|
+
fs15.linkSync(temp, path17.join(dir, name2));
|
|
9134
|
+
fsyncDir(dir);
|
|
9135
|
+
return true;
|
|
9136
|
+
} catch (error) {
|
|
9137
|
+
if (error.code === "EEXIST")
|
|
9138
|
+
return false;
|
|
9139
|
+
throw error;
|
|
9140
|
+
} finally {
|
|
9141
|
+
fs15.rmSync(temp, { force: true });
|
|
9142
|
+
}
|
|
9143
|
+
}
|
|
9144
|
+
function writeReplace(dir, name2, body, mode) {
|
|
9145
|
+
const target2 = path17.join(dir, name2);
|
|
9146
|
+
try {
|
|
9147
|
+
if (!fs15.lstatSync(target2).isFile())
|
|
9148
|
+
throw new CourierError("unsafe-target", "refusing to replace a non-regular file");
|
|
9149
|
+
} catch (error) {
|
|
9150
|
+
if (error.code !== "ENOENT")
|
|
9151
|
+
throw error;
|
|
9152
|
+
}
|
|
9153
|
+
const temp = writeTemp(dir, name2, body, mode);
|
|
9154
|
+
try {
|
|
9155
|
+
fs15.renameSync(temp, target2);
|
|
9156
|
+
fsyncDir(dir);
|
|
9157
|
+
} catch (error) {
|
|
9158
|
+
fs15.rmSync(temp, { force: true });
|
|
9159
|
+
throw error;
|
|
9160
|
+
}
|
|
9161
|
+
}
|
|
9162
|
+
function parseRecord(text) {
|
|
9163
|
+
if (/^\d+\n?$/.test(text)) {
|
|
9164
|
+
const pid = Number(text);
|
|
9165
|
+
return Number.isInteger(pid) && pid > 0 ? { pid, nonce: text } : void 0;
|
|
9166
|
+
}
|
|
9167
|
+
try {
|
|
9168
|
+
const value2 = JSON.parse(text);
|
|
9169
|
+
if (typeof value2 !== "object" || value2 === null)
|
|
9170
|
+
return void 0;
|
|
9171
|
+
const { pid, nonce } = value2;
|
|
9172
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
|
|
9173
|
+
return void 0;
|
|
9174
|
+
if (typeof nonce !== "string" || nonce.length === 0)
|
|
9175
|
+
return void 0;
|
|
9176
|
+
return { pid, nonce };
|
|
9177
|
+
} catch {
|
|
9178
|
+
return void 0;
|
|
9179
|
+
}
|
|
9180
|
+
}
|
|
9181
|
+
function recordBody(record) {
|
|
9182
|
+
return `${JSON.stringify({ pid: record.pid, nonce: record.nonce })}
|
|
9183
|
+
`;
|
|
9184
|
+
}
|
|
9185
|
+
function readRecordFile(file) {
|
|
9186
|
+
try {
|
|
9187
|
+
return parseRecord(fs15.readFileSync(file, "utf8"));
|
|
9188
|
+
} catch {
|
|
9189
|
+
return void 0;
|
|
9190
|
+
}
|
|
9191
|
+
}
|
|
9192
|
+
function readLock(file) {
|
|
9193
|
+
let text;
|
|
9194
|
+
try {
|
|
9195
|
+
text = fs15.readFileSync(file, "utf8");
|
|
9196
|
+
} catch (error) {
|
|
9197
|
+
if (error.code === "ENOENT")
|
|
9198
|
+
return { kind: "missing" };
|
|
9199
|
+
throw error;
|
|
9200
|
+
}
|
|
9201
|
+
const record = parseRecord(text);
|
|
9202
|
+
return record ? { kind: "held", record } : { kind: "corrupt" };
|
|
9203
|
+
}
|
|
9204
|
+
function pidAlive2(pid) {
|
|
9205
|
+
try {
|
|
9206
|
+
process.kill(pid, 0);
|
|
9207
|
+
return true;
|
|
9208
|
+
} catch (error) {
|
|
9209
|
+
return error.code === "EPERM";
|
|
9210
|
+
}
|
|
9211
|
+
}
|
|
9212
|
+
var MAX_BREAK_GUARDS = 8;
|
|
9213
|
+
function acquireLock(file, opts = {}) {
|
|
9214
|
+
const dir = path17.dirname(file);
|
|
9215
|
+
const name2 = path17.basename(file);
|
|
9216
|
+
const alive = opts.alive ?? pidAlive2;
|
|
9217
|
+
const self2 = { pid: opts.pid ?? process.pid, nonce: randomBytes3(6).toString("hex") };
|
|
9218
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
9219
|
+
if (writeNew(dir, name2, recordBody(self2), 384)) {
|
|
9220
|
+
return () => {
|
|
9221
|
+
const current = readLock(file);
|
|
9222
|
+
if (current.kind === "held" && current.record.nonce === self2.nonce)
|
|
9223
|
+
fs15.rmSync(file, { force: true });
|
|
9224
|
+
};
|
|
9225
|
+
}
|
|
9226
|
+
const seen = readLock(file);
|
|
9227
|
+
if (seen.kind === "missing")
|
|
9228
|
+
continue;
|
|
9229
|
+
if (seen.kind === "corrupt" || alive(seen.record.pid)) {
|
|
9230
|
+
throw new CourierError("locked", "another courier intake is running");
|
|
9231
|
+
}
|
|
9232
|
+
opts.afterObserve?.();
|
|
9233
|
+
if (!breakDead(file, seen.record, self2, alive)) {
|
|
9234
|
+
throw new CourierError("locked", "another courier intake is running");
|
|
9235
|
+
}
|
|
9236
|
+
}
|
|
9237
|
+
throw new CourierError("locked", "another courier intake is running");
|
|
9238
|
+
}
|
|
9239
|
+
function breakDead(file, stale, self2, alive) {
|
|
9240
|
+
const dir = path17.dirname(file);
|
|
9241
|
+
const tag = `${path17.basename(file)}.break.${stale.nonce.trim().replace(/[^A-Za-z0-9_-]/g, "_")}.`;
|
|
9242
|
+
const body = recordBody(self2);
|
|
9243
|
+
let claimed = false;
|
|
9244
|
+
for (let k2 = 1; k2 <= MAX_BREAK_GUARDS && !claimed; k2++) {
|
|
9245
|
+
const candidate = `${tag}${k2}`;
|
|
9246
|
+
if (writeNew(dir, candidate, body, 384)) {
|
|
9247
|
+
claimed = true;
|
|
9248
|
+
break;
|
|
9249
|
+
}
|
|
9250
|
+
const holder = readRecordFile(path17.join(dir, candidate));
|
|
9251
|
+
if (holder && alive(holder.pid))
|
|
9252
|
+
return false;
|
|
9253
|
+
}
|
|
9254
|
+
if (!claimed)
|
|
9255
|
+
return false;
|
|
9256
|
+
try {
|
|
9257
|
+
const now = readLock(file);
|
|
9258
|
+
if (now.kind === "held" && now.record.nonce === stale.nonce)
|
|
9259
|
+
fs15.rmSync(file, { force: true });
|
|
9260
|
+
return true;
|
|
9261
|
+
} finally {
|
|
9262
|
+
for (const entry of fs15.readdirSync(dir)) {
|
|
9263
|
+
if (entry.startsWith(tag))
|
|
9264
|
+
fs15.rmSync(path17.join(dir, entry), { force: true });
|
|
9265
|
+
}
|
|
9266
|
+
}
|
|
9267
|
+
}
|
|
9268
|
+
function emptyState() {
|
|
9269
|
+
return { v: 1, peers: /* @__PURE__ */ Object.create(null) };
|
|
9270
|
+
}
|
|
9271
|
+
function readState(file) {
|
|
9272
|
+
let raw;
|
|
9273
|
+
try {
|
|
9274
|
+
raw = fs15.readFileSync(file, "utf8");
|
|
9275
|
+
} catch (error) {
|
|
9276
|
+
if (error.code === "ENOENT")
|
|
9277
|
+
return emptyState();
|
|
9278
|
+
throw error;
|
|
9279
|
+
}
|
|
9280
|
+
let parsed;
|
|
9281
|
+
try {
|
|
9282
|
+
parsed = JSON.parse(raw);
|
|
9283
|
+
} catch {
|
|
9284
|
+
throw new CourierError("bad-state", "intake state is not valid JSON");
|
|
9285
|
+
}
|
|
9286
|
+
if (!isRecord(parsed) || parsed.v !== 1 || !isRecord(parsed.peers))
|
|
9287
|
+
throw new CourierError("bad-state", "intake state has an unknown shape");
|
|
9288
|
+
const peers = /* @__PURE__ */ Object.create(null);
|
|
9289
|
+
for (const [peerId, entry] of Object.entries(parsed.peers)) {
|
|
9290
|
+
if (!isRecord(entry) || !Array.isArray(entry.receipts) || entry.cursor !== void 0 && typeof entry.cursor !== "string") {
|
|
9291
|
+
throw new CourierError("bad-state", "intake state has an unknown shape");
|
|
9292
|
+
}
|
|
9293
|
+
const peer = { receipts: entry.receipts.map(readReceipt) };
|
|
9294
|
+
if (typeof entry.cursor === "string")
|
|
9295
|
+
peer.cursor = entry.cursor;
|
|
9296
|
+
peers[peerId] = peer;
|
|
9297
|
+
}
|
|
9298
|
+
return { v: 1, peers };
|
|
9299
|
+
}
|
|
9300
|
+
function readReceipt(raw, index) {
|
|
9301
|
+
if (typeof raw === "number")
|
|
9302
|
+
return { id: `legacy-${index}`, at: raw };
|
|
9303
|
+
if (isRecord(raw) && typeof raw.id === "string" && typeof raw.at === "number")
|
|
9304
|
+
return { id: raw.id, at: raw.at };
|
|
9305
|
+
throw new CourierError("bad-state", "intake state has an unknown shape");
|
|
9306
|
+
}
|
|
9307
|
+
function writeState(dir, file, state) {
|
|
9308
|
+
writeReplace(dir, path17.basename(file), `${JSON.stringify(state)}
|
|
9309
|
+
`, 384);
|
|
9310
|
+
}
|
|
9311
|
+
|
|
9312
|
+
// ../courier/dist/intake/intake.js
|
|
9313
|
+
var STATE_FILE = "intake-state.json";
|
|
9314
|
+
var LOCK_FILE = "intake.lock";
|
|
9315
|
+
var HOUR_MS = 60 * 6e4;
|
|
9316
|
+
var DAY_MS3 = 24 * HOUR_MS;
|
|
9317
|
+
var MAX_SKEW_MS = 10 * 6e4;
|
|
9318
|
+
var STALE_MS = 7 * DAY_MS3;
|
|
9319
|
+
var DONE_TTL_MS = 14 * DAY_MS3;
|
|
9320
|
+
var OVERFLOW_TTL_MS = STALE_MS;
|
|
9321
|
+
var OVERFLOW_MAX_FILES = 200;
|
|
9322
|
+
var OVERFLOW_MAX_BYTES = 1024 * 1024;
|
|
9323
|
+
var MIRROR_BATCH = 200;
|
|
9324
|
+
var JANITOR_BATCH = 500;
|
|
9325
|
+
var RATE_WINDOW_MS = HOUR_MS;
|
|
9326
|
+
var MAX_QUEUE_CHARS = 8e3;
|
|
9327
|
+
var MAX_QUEUE_BYTES = 16 * 1024;
|
|
9328
|
+
var RATE_TEXT = "This peer went over its inbound rate limit; further messages this hour are held outside the queue.";
|
|
9329
|
+
var ULID_RE = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/;
|
|
9330
|
+
var SAFE_ID2 = /^[0-9A-Za-z_-]{1,64}$/;
|
|
9331
|
+
var MSG_KEY2 = /\/threads\/([^/]+)\/msg\/([^/]+)\.json$/;
|
|
9332
|
+
var LINK = /(?:https?|ftp):\/\/|\bwww\.[a-z0-9-]+\./i;
|
|
9333
|
+
var ENCODED = /[A-Za-z0-9+\/=]{64,}/;
|
|
9334
|
+
var REQUEST = /(?:^|[.!?\n]\s*|\bplease\s+)(?:run|merge|send|open|forward|delete|reveal|deploy)\b/i;
|
|
9335
|
+
var SECRET_REQUEST = /\b(?:share|send|give|paste|reveal|tell|show|post)\b[^.!?\n]{0,40}\b(?:keys?|tokens?|passwords?|credentials?|secrets?|seed phrase)\b/i;
|
|
9336
|
+
var INSTRUCTION_PHRASES = [
|
|
9337
|
+
"ignore previous",
|
|
9338
|
+
"ignore all previous",
|
|
9339
|
+
"disregard previous",
|
|
9340
|
+
"forget your instructions",
|
|
9341
|
+
"you are now",
|
|
9342
|
+
"new instructions",
|
|
9343
|
+
"system prompt",
|
|
9344
|
+
"override your"
|
|
9345
|
+
];
|
|
9346
|
+
function capUtf8(text, maxBytes) {
|
|
9347
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes)
|
|
9348
|
+
return { text, truncated: false };
|
|
9349
|
+
let out = "";
|
|
9350
|
+
let bytes = 0;
|
|
9351
|
+
for (const ch of text) {
|
|
9352
|
+
const n = Buffer.byteLength(ch, "utf8");
|
|
9353
|
+
if (bytes + n > maxBytes)
|
|
9354
|
+
break;
|
|
9355
|
+
out += ch;
|
|
9356
|
+
bytes += n;
|
|
9357
|
+
}
|
|
9358
|
+
return { text: out, truncated: true };
|
|
9359
|
+
}
|
|
9360
|
+
function flagsFor(input) {
|
|
9361
|
+
const found = /* @__PURE__ */ new Set();
|
|
9362
|
+
if (!input.verdict.allowed && input.verdict.reason === "too-long")
|
|
9363
|
+
found.add("too-long");
|
|
9364
|
+
if (hiddenTextProblem(input.text, true) || !input.verdict.allowed && input.verdict.reason !== "too-long")
|
|
9365
|
+
found.add("unicode");
|
|
9366
|
+
const lower = input.text.toLowerCase();
|
|
9367
|
+
if (LINK.test(input.text))
|
|
9368
|
+
found.add("link");
|
|
9369
|
+
if (INSTRUCTION_PHRASES.some((phrase) => lower.includes(phrase)))
|
|
9370
|
+
found.add("instruction");
|
|
9371
|
+
if (REQUEST.test(input.text))
|
|
9372
|
+
found.add("request");
|
|
9373
|
+
if (SECRET_REQUEST.test(input.text))
|
|
9374
|
+
found.add("secret-request");
|
|
9375
|
+
if (ENCODED.test(input.text))
|
|
9376
|
+
found.add("encoded");
|
|
9377
|
+
if (input.overRate)
|
|
9378
|
+
found.add("rate");
|
|
9379
|
+
if (input.truncated)
|
|
9380
|
+
found.add("size");
|
|
9381
|
+
return Object.keys(FLAG_TABLE).filter((flag) => found.has(flag));
|
|
9382
|
+
}
|
|
9383
|
+
function admit(receipts, id, at, cap2) {
|
|
9384
|
+
const recent = receipts.filter((r) => r.at > at - RATE_WINDOW_MS);
|
|
9385
|
+
if (recent.length >= cap2)
|
|
9386
|
+
return { admitted: false, remaining: 0, receipts: recent.slice(-cap2) };
|
|
9387
|
+
return { admitted: true, remaining: cap2 - recent.length - 1, receipts: [...recent, { id, at }].slice(-cap2) };
|
|
9388
|
+
}
|
|
9389
|
+
var reconciledPeers = /* @__PURE__ */ new WeakMap();
|
|
9390
|
+
function reconciledSet(state) {
|
|
9391
|
+
let set = reconciledPeers.get(state);
|
|
9392
|
+
if (!set) {
|
|
9393
|
+
set = /* @__PURE__ */ new Set();
|
|
9394
|
+
reconciledPeers.set(state, set);
|
|
9395
|
+
}
|
|
9396
|
+
return set;
|
|
9397
|
+
}
|
|
9398
|
+
function reconcileReceipts(input, peer, receipts, cap2) {
|
|
9399
|
+
const now = input.now();
|
|
9400
|
+
const byId = /* @__PURE__ */ new Map();
|
|
9401
|
+
for (const r of receipts)
|
|
9402
|
+
if (!byId.has(r.id))
|
|
9403
|
+
byId.set(r.id, r);
|
|
9404
|
+
for (const root of [input.queueRoot, input.doneRoot]) {
|
|
9405
|
+
const dir = path18.join(root, peer);
|
|
9406
|
+
let names;
|
|
9407
|
+
try {
|
|
9408
|
+
names = fs16.readdirSync(dir);
|
|
9409
|
+
} catch (error) {
|
|
9410
|
+
if (error.code === "ENOENT")
|
|
9411
|
+
continue;
|
|
9412
|
+
throw error;
|
|
9413
|
+
}
|
|
9414
|
+
for (const name2 of names) {
|
|
9415
|
+
if (!name2.endsWith(".json"))
|
|
9416
|
+
continue;
|
|
9417
|
+
const id = name2.slice(0, -".json".length);
|
|
9418
|
+
if (!ULID_RE.test(id) || byId.has(id))
|
|
9419
|
+
continue;
|
|
9420
|
+
const file = path18.join(dir, name2);
|
|
9421
|
+
let stat;
|
|
9422
|
+
try {
|
|
9423
|
+
stat = fs16.lstatSync(file);
|
|
9424
|
+
} catch {
|
|
9425
|
+
continue;
|
|
9426
|
+
}
|
|
9427
|
+
if (!stat.isFile() || stat.mtimeMs < now - RATE_WINDOW_MS - 6e4)
|
|
9428
|
+
continue;
|
|
9429
|
+
let record;
|
|
9430
|
+
try {
|
|
9431
|
+
record = JSON.parse(fs16.readFileSync(file, "utf8"));
|
|
9432
|
+
} catch {
|
|
9433
|
+
continue;
|
|
9434
|
+
}
|
|
9435
|
+
if (!isRecord(record) || typeof record.receivedAt !== "string")
|
|
9436
|
+
continue;
|
|
9437
|
+
const at = Date.parse(record.receivedAt);
|
|
9438
|
+
if (Number.isFinite(at))
|
|
9439
|
+
byId.set(id, { id, at });
|
|
9440
|
+
}
|
|
9441
|
+
}
|
|
9442
|
+
return [...byId.values()].filter((r) => r.at > now - RATE_WINDOW_MS).sort((a, b) => a.at - b.at).slice(-cap2);
|
|
9443
|
+
}
|
|
9444
|
+
function persist(input, peer) {
|
|
9445
|
+
try {
|
|
9446
|
+
writeState(input.stateDir, path18.join(input.stateDir, STATE_FILE), input.state);
|
|
9447
|
+
} catch (error) {
|
|
9448
|
+
reconciledSet(input.state).delete(peer);
|
|
9449
|
+
throw error;
|
|
9450
|
+
}
|
|
9451
|
+
}
|
|
9452
|
+
function presentIn(input, peer, name2) {
|
|
9453
|
+
for (const root of [input.queueRoot, input.doneRoot, input.overflowRoot]) {
|
|
9454
|
+
try {
|
|
9455
|
+
fs16.lstatSync(path18.join(root, peer, name2));
|
|
9456
|
+
return true;
|
|
9457
|
+
} catch (error) {
|
|
9458
|
+
if (error.code !== "ENOENT")
|
|
9459
|
+
throw error;
|
|
9460
|
+
}
|
|
9461
|
+
}
|
|
9462
|
+
return false;
|
|
9463
|
+
}
|
|
9464
|
+
function pathExists(file) {
|
|
9465
|
+
try {
|
|
9466
|
+
fs16.lstatSync(file);
|
|
9467
|
+
return true;
|
|
9468
|
+
} catch (error) {
|
|
9469
|
+
if (error.code === "ENOENT")
|
|
9470
|
+
return false;
|
|
9471
|
+
throw error;
|
|
9472
|
+
}
|
|
9473
|
+
}
|
|
9474
|
+
function markDone(input, peer, msgId, outcome) {
|
|
9475
|
+
const dir = path18.join(input.doneRoot, peer);
|
|
9476
|
+
ensureRealDir(dir, 448);
|
|
9477
|
+
const marker = { v: 1, msgId, peerId: peer, outcome, markedAt: new Date(input.now()).toISOString() };
|
|
9478
|
+
return writeNew(dir, `${msgId}.json`, `${JSON.stringify(marker)}
|
|
9479
|
+
`, 384);
|
|
9480
|
+
}
|
|
9481
|
+
function mirrorMessages(handle, keys, log) {
|
|
9482
|
+
const messages = [];
|
|
9483
|
+
for (const key of keys) {
|
|
9484
|
+
const match = MSG_KEY2.exec(key);
|
|
9485
|
+
if (!match)
|
|
9486
|
+
continue;
|
|
9487
|
+
const [, threadId, msgId] = match;
|
|
9488
|
+
if (!ULID_RE.test(threadId) || !ULID_RE.test(msgId)) {
|
|
9489
|
+
log("intake-skip", { peer: handle, code: "bad-id" });
|
|
9490
|
+
continue;
|
|
9491
|
+
}
|
|
9492
|
+
messages.push({ key, threadId, msgId });
|
|
9493
|
+
}
|
|
9494
|
+
return messages;
|
|
9495
|
+
}
|
|
9496
|
+
function writeRateRecord(input, peer, senderName, tier, at) {
|
|
9497
|
+
const msgId = `rate-${Math.floor(at / RATE_WINDOW_MS)}`;
|
|
9498
|
+
const dir = path18.join(input.queueRoot, peer);
|
|
9499
|
+
ensureRealDir(dir, 1512);
|
|
9500
|
+
const record = {
|
|
9501
|
+
v: 1,
|
|
9502
|
+
msgId,
|
|
9503
|
+
peerId: peer,
|
|
9504
|
+
threadId: "rate",
|
|
9505
|
+
sender: { id: peer, displayName: senderName },
|
|
9506
|
+
receivedAt: new Date(at).toISOString(),
|
|
9507
|
+
text: RATE_TEXT,
|
|
9508
|
+
cheapChecks: {
|
|
9509
|
+
textVerdict: textVerdict(RATE_TEXT, "multiline", MAX_QUEUE_CHARS),
|
|
9510
|
+
tier,
|
|
9511
|
+
peerRatePerHourRemaining: 0,
|
|
9512
|
+
flags: ["rate"]
|
|
9513
|
+
}
|
|
9514
|
+
};
|
|
9515
|
+
if (presentIn(input, peer, `${msgId}.json`))
|
|
9516
|
+
return false;
|
|
9517
|
+
const created = writeNew(dir, `${msgId}.json`, `${JSON.stringify(record)}
|
|
9518
|
+
`, 416);
|
|
9519
|
+
if (created)
|
|
9520
|
+
input.log("peer-over-rate", { peer, msgId });
|
|
9521
|
+
return created;
|
|
9522
|
+
}
|
|
9523
|
+
function pickBatch(items, cursor, cap2, floor) {
|
|
9524
|
+
if (cap2 < 1)
|
|
9525
|
+
return [];
|
|
9526
|
+
const after = [];
|
|
9527
|
+
const wrapped = [];
|
|
9528
|
+
const insertSorted = (list, item) => {
|
|
9529
|
+
if (list.length === cap2 && item.msgId >= list[cap2 - 1].msgId)
|
|
9530
|
+
return;
|
|
9531
|
+
let i = list.length;
|
|
9532
|
+
while (i > 0 && list[i - 1].msgId > item.msgId)
|
|
9533
|
+
i--;
|
|
9534
|
+
list.splice(i, 0, item);
|
|
9535
|
+
if (list.length > cap2)
|
|
9536
|
+
list.pop();
|
|
9537
|
+
};
|
|
9538
|
+
for (const item of items) {
|
|
9539
|
+
if (item.msgId < floor)
|
|
9540
|
+
continue;
|
|
9541
|
+
if (item.msgId > cursor)
|
|
9542
|
+
insertSorted(after, item);
|
|
9543
|
+
else
|
|
9544
|
+
insertSorted(wrapped, item);
|
|
9545
|
+
}
|
|
9546
|
+
return [...after, ...wrapped].slice(0, cap2);
|
|
9547
|
+
}
|
|
9548
|
+
async function intakePeer(input, peer) {
|
|
9549
|
+
const policy = policyOf(input.peers, peer.handle);
|
|
9550
|
+
const ps = Object.hasOwn(input.state.peers, peer.handle) ? input.state.peers[peer.handle] : { receipts: [] };
|
|
9551
|
+
input.state.peers[peer.handle] = ps;
|
|
9552
|
+
const reconciled = reconciledSet(input.state);
|
|
9553
|
+
if (!reconciled.has(peer.handle)) {
|
|
9554
|
+
ps.receipts = reconcileReceipts(input, peer.handle, ps.receipts, policy.ratePerHour);
|
|
9555
|
+
reconciled.add(peer.handle);
|
|
9556
|
+
}
|
|
9557
|
+
const senderName = typeof peer.label === "string" && textVerdict(peer.label, "summary").allowed ? peer.label : peer.handle;
|
|
9558
|
+
const keys = await listAll(input.kv, `${k.mirrorPrefix}${peer.handle}/threads/`);
|
|
9559
|
+
const all = mirrorMessages(peer.handle, keys, input.log);
|
|
9560
|
+
const cap2 = input.batch ?? MIRROR_BATCH;
|
|
9561
|
+
const floor = encodeTime(input.now() - STALE_MS, 10) + "0".repeat(16);
|
|
9562
|
+
const picked = pickBatch(all, ps.cursor ?? "", cap2, floor);
|
|
9563
|
+
let written = 0;
|
|
9564
|
+
let future = 0;
|
|
9565
|
+
let overflowed = false;
|
|
9566
|
+
try {
|
|
9567
|
+
for (const c of picked) {
|
|
9568
|
+
try {
|
|
9569
|
+
if (presentIn(input, peer.handle, `${c.msgId}.json`))
|
|
9570
|
+
continue;
|
|
9571
|
+
const at = input.now();
|
|
9572
|
+
let sentAt;
|
|
9573
|
+
try {
|
|
9574
|
+
sentAt = decodeTime(c.msgId);
|
|
9575
|
+
} catch {
|
|
9576
|
+
input.log("intake-skip", { peer: peer.handle, code: "bad-id" });
|
|
9577
|
+
markDone(input, peer.handle, c.msgId, "bad-id");
|
|
9578
|
+
continue;
|
|
9579
|
+
}
|
|
9580
|
+
if (sentAt > at + MAX_SKEW_MS) {
|
|
9581
|
+
future++;
|
|
9582
|
+
continue;
|
|
9583
|
+
}
|
|
9584
|
+
const got = await getJson(input.kv, c.key);
|
|
9585
|
+
if (!got || !isRecord(got.value) || got.value.messageId !== c.msgId || got.value.contextId !== c.threadId) {
|
|
9586
|
+
input.log("intake-skip", { peer: peer.handle, code: "mismatch" });
|
|
9587
|
+
markDone(input, peer.handle, c.msgId, "mismatch");
|
|
9588
|
+
continue;
|
|
9589
|
+
}
|
|
9590
|
+
const { admitted, remaining, receipts } = admit(ps.receipts, c.msgId, at, policy.ratePerHour);
|
|
9591
|
+
const parts = Array.isArray(got.value.parts) ? got.value.parts : [];
|
|
9592
|
+
const texts = [];
|
|
9593
|
+
for (const part of parts)
|
|
9594
|
+
if (isRecord(part) && typeof part.text === "string")
|
|
9595
|
+
texts.push(part.text);
|
|
9596
|
+
const text = texts.join("\n");
|
|
9597
|
+
const verdict = textVerdict(text, "multiline", MAX_QUEUE_CHARS);
|
|
9598
|
+
const capped = capUtf8(text, MAX_QUEUE_BYTES);
|
|
9599
|
+
const flags = flagsFor({ text, verdict, overRate: !admitted, truncated: capped.truncated });
|
|
9600
|
+
const record = {
|
|
9601
|
+
v: 1,
|
|
9602
|
+
msgId: c.msgId,
|
|
9603
|
+
peerId: peer.handle,
|
|
9604
|
+
threadId: c.threadId,
|
|
9605
|
+
sender: { id: peer.handle, displayName: senderName },
|
|
9606
|
+
receivedAt: new Date(at).toISOString(),
|
|
9607
|
+
text: capped.text,
|
|
9608
|
+
cheapChecks: { textVerdict: verdict, tier: policy.tier, peerRatePerHourRemaining: remaining, flags }
|
|
9609
|
+
};
|
|
9610
|
+
const body = `${JSON.stringify(record)}
|
|
9611
|
+
`;
|
|
9612
|
+
if (admitted) {
|
|
9613
|
+
const dir = path18.join(input.queueRoot, peer.handle);
|
|
9614
|
+
ensureRealDir(dir, 1512);
|
|
9615
|
+
const name2 = `${c.msgId}.json`;
|
|
9616
|
+
let published;
|
|
9617
|
+
try {
|
|
9618
|
+
published = writeNew(dir, name2, body, 416);
|
|
9619
|
+
} catch (error) {
|
|
9620
|
+
if (pathExists(path18.join(dir, name2)))
|
|
9621
|
+
ps.receipts = receipts;
|
|
9622
|
+
throw error;
|
|
9623
|
+
}
|
|
9624
|
+
if (published) {
|
|
9625
|
+
ps.receipts = receipts;
|
|
9626
|
+
persist(input, peer.handle);
|
|
9627
|
+
written++;
|
|
9628
|
+
input.log("intake-queued", { peer: peer.handle, msgId: c.msgId, tier: policy.tier, flags });
|
|
9629
|
+
}
|
|
9630
|
+
} else {
|
|
9631
|
+
const dir = path18.join(input.overflowRoot, peer.handle);
|
|
9632
|
+
ensureRealDir(dir, 448);
|
|
9633
|
+
writeNew(dir, `${c.msgId}.json`, body, 384);
|
|
9634
|
+
overflowed = true;
|
|
9635
|
+
ps.receipts = receipts;
|
|
9636
|
+
persist(input, peer.handle);
|
|
9637
|
+
if (writeRateRecord(input, peer.handle, senderName, policy.tier, at))
|
|
9638
|
+
written++;
|
|
9639
|
+
}
|
|
9640
|
+
} catch (error) {
|
|
9641
|
+
input.log("intake-error", { peer: peer.handle, msgId: c.msgId, code: errInfo(error).code ?? "error" });
|
|
9642
|
+
}
|
|
9643
|
+
}
|
|
9644
|
+
} finally {
|
|
9645
|
+
if (picked.length > 0)
|
|
9646
|
+
ps.cursor = picked[picked.length - 1].msgId;
|
|
9647
|
+
if (overflowed)
|
|
9648
|
+
capOverflow(input, peer.handle);
|
|
9649
|
+
persist(input, peer.handle);
|
|
9650
|
+
}
|
|
9651
|
+
if (future > 0)
|
|
9652
|
+
input.log("intake-skip", { peer: peer.handle, code: "stale", direction: "future", count: future });
|
|
9653
|
+
return written;
|
|
9654
|
+
}
|
|
9655
|
+
function jsonNames(dir) {
|
|
9656
|
+
return fs16.readdirSync(dir).filter((name2) => name2.endsWith(".json")).sort();
|
|
9657
|
+
}
|
|
9658
|
+
function nameTime(name2) {
|
|
9659
|
+
const base = name2.slice(0, -".json".length);
|
|
9660
|
+
if (ULID_RE.test(base))
|
|
9661
|
+
return decodeTime(base);
|
|
9662
|
+
const rate = /^rate-(\d+)$/.exec(base);
|
|
9663
|
+
return rate ? Number(rate[1]) * RATE_WINDOW_MS : void 0;
|
|
9664
|
+
}
|
|
9665
|
+
function retireOverflow(input, peer, name2) {
|
|
9666
|
+
const msgId = name2.slice(0, -".json".length);
|
|
9667
|
+
if (ULID_RE.test(msgId))
|
|
9668
|
+
markDone(input, peer, msgId, "overflow");
|
|
9669
|
+
fs16.unlinkSync(path18.join(input.overflowRoot, peer, name2));
|
|
9670
|
+
}
|
|
9671
|
+
function capOverflow(input, peer) {
|
|
9672
|
+
const dir = path18.join(input.overflowRoot, peer);
|
|
9673
|
+
const names = jsonNames(dir);
|
|
9674
|
+
const excess = Math.min(Math.max(0, names.length - OVERFLOW_MAX_FILES), JANITOR_BATCH);
|
|
9675
|
+
for (const name2 of names.slice(0, excess))
|
|
9676
|
+
retireOverflow(input, peer, name2);
|
|
9677
|
+
const kept = names.slice(excess);
|
|
9678
|
+
if (kept.length > OVERFLOW_MAX_FILES)
|
|
9679
|
+
return;
|
|
9680
|
+
const sizes = kept.map((name2) => fs16.statSync(path18.join(dir, name2)).size);
|
|
9681
|
+
let bytes = sizes.reduce((sum, size) => sum + size, 0);
|
|
9682
|
+
for (let i = 0; i < kept.length && bytes > OVERFLOW_MAX_BYTES; i++) {
|
|
9683
|
+
retireOverflow(input, peer, kept[i]);
|
|
9684
|
+
bytes -= sizes[i];
|
|
9685
|
+
}
|
|
9686
|
+
}
|
|
9687
|
+
function peerDirs(root) {
|
|
9688
|
+
return fs16.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && SAFE_ID2.test(entry.name)).map((entry) => entry.name);
|
|
9689
|
+
}
|
|
9690
|
+
function runJanitor(input) {
|
|
9691
|
+
const now = input.now();
|
|
9692
|
+
for (const peer of peerDirs(input.queueRoot)) {
|
|
9693
|
+
const dir = path18.join(input.queueRoot, peer);
|
|
9694
|
+
let budget = JANITOR_BATCH;
|
|
9695
|
+
for (const name2 of jsonNames(dir)) {
|
|
9696
|
+
const at = nameTime(name2);
|
|
9697
|
+
if (at === void 0 || !(at < now - STALE_MS))
|
|
9698
|
+
continue;
|
|
9699
|
+
if (budget-- <= 0)
|
|
9700
|
+
break;
|
|
9701
|
+
const file = path18.join(dir, name2);
|
|
9702
|
+
const body = fs16.readFileSync(file, "utf8");
|
|
9703
|
+
try {
|
|
9704
|
+
if (!isRecord(JSON.parse(body)))
|
|
9705
|
+
continue;
|
|
9706
|
+
} catch {
|
|
9707
|
+
continue;
|
|
9708
|
+
}
|
|
9709
|
+
const doneDir = path18.join(input.doneRoot, peer);
|
|
9710
|
+
ensureRealDir(doneDir, 448);
|
|
9711
|
+
writeNew(doneDir, name2, body, 384);
|
|
9712
|
+
fs16.unlinkSync(file);
|
|
9713
|
+
}
|
|
9714
|
+
}
|
|
9715
|
+
for (const peer of peerDirs(input.doneRoot)) {
|
|
9716
|
+
const dir = path18.join(input.doneRoot, peer);
|
|
9717
|
+
let budget = JANITOR_BATCH;
|
|
9718
|
+
for (const name2 of jsonNames(dir)) {
|
|
9719
|
+
const at = nameTime(name2);
|
|
9720
|
+
if (at === void 0 || !(at < now - DONE_TTL_MS))
|
|
9721
|
+
continue;
|
|
9722
|
+
if (budget-- <= 0)
|
|
9723
|
+
break;
|
|
9724
|
+
fs16.unlinkSync(path18.join(dir, name2));
|
|
9725
|
+
}
|
|
9726
|
+
}
|
|
9727
|
+
for (const peer of peerDirs(input.overflowRoot)) {
|
|
9728
|
+
const dir = path18.join(input.overflowRoot, peer);
|
|
9729
|
+
let budget = JANITOR_BATCH;
|
|
9730
|
+
for (const name2 of jsonNames(dir)) {
|
|
9731
|
+
const at = nameTime(name2);
|
|
9732
|
+
if (at === void 0 || !(at < now - OVERFLOW_TTL_MS))
|
|
9733
|
+
continue;
|
|
9734
|
+
if (budget-- <= 0)
|
|
9735
|
+
break;
|
|
9736
|
+
retireOverflow(input, peer, name2);
|
|
9737
|
+
}
|
|
9738
|
+
capOverflow(input, peer);
|
|
9739
|
+
}
|
|
9740
|
+
}
|
|
9741
|
+
async function intakeCycle(input) {
|
|
9742
|
+
for (const id of Object.keys(input.state.peers)) {
|
|
9743
|
+
if (!Object.hasOwn(input.profile.peers, id))
|
|
9744
|
+
delete input.state.peers[id];
|
|
9745
|
+
}
|
|
9746
|
+
runJanitor(input);
|
|
9747
|
+
let written = 0;
|
|
9748
|
+
for (const peer of Object.values(input.profile.peers)) {
|
|
9749
|
+
if (peer.status !== "paired")
|
|
9750
|
+
continue;
|
|
9751
|
+
if (!SAFE_ID2.test(peer.handle)) {
|
|
9752
|
+
input.log("intake-skip", { code: "bad-handle" });
|
|
9753
|
+
continue;
|
|
9754
|
+
}
|
|
9755
|
+
try {
|
|
9756
|
+
written += await intakePeer(input, peer);
|
|
9757
|
+
} catch (error) {
|
|
9758
|
+
input.log("intake-error", { peer: peer.handle, code: errInfo(error).code ?? "error" });
|
|
9759
|
+
}
|
|
9760
|
+
}
|
|
9761
|
+
return written;
|
|
9762
|
+
}
|
|
9763
|
+
async function initFromNow(input, force) {
|
|
9764
|
+
const paired = Object.values(input.profile.peers).filter((peer) => peer.status === "paired" && SAFE_ID2.test(peer.handle));
|
|
9765
|
+
const hasDone = paired.some((peer) => {
|
|
9766
|
+
const dir = path18.join(input.doneRoot, peer.handle);
|
|
9767
|
+
return fs16.existsSync(dir) && fs16.readdirSync(dir).length > 0;
|
|
9768
|
+
});
|
|
9769
|
+
if (hasDone && !force)
|
|
9770
|
+
throw new CourierError("init-exists", "done/ already has entries for a paired peer; pass --force to mark again");
|
|
9771
|
+
const marked = /* @__PURE__ */ new Map();
|
|
9772
|
+
for (const peer of paired) {
|
|
9773
|
+
const keys = await listAll(input.kv, `${k.mirrorPrefix}${peer.handle}/threads/`);
|
|
9774
|
+
let count = 0;
|
|
9775
|
+
for (const c of mirrorMessages(peer.handle, keys, input.log)) {
|
|
9776
|
+
if (markDone(input, peer.handle, c.msgId, "init"))
|
|
9777
|
+
count++;
|
|
9778
|
+
}
|
|
9779
|
+
marked.set(peer.handle, count);
|
|
9780
|
+
input.log("intake-init", { peer: peer.handle, marked: count });
|
|
9781
|
+
}
|
|
9782
|
+
return marked;
|
|
9783
|
+
}
|
|
9784
|
+
function intervalSeconds(raw) {
|
|
9785
|
+
if (raw === void 0)
|
|
9786
|
+
return 60;
|
|
9787
|
+
const n = /^\d+$/.test(raw) ? Number(raw) : NaN;
|
|
9788
|
+
if (!(n >= 5 && n <= 3600))
|
|
9789
|
+
throw new CourierError("usage", "--interval must be an integer from 5 to 3600");
|
|
9790
|
+
return n;
|
|
9791
|
+
}
|
|
9792
|
+
async function cmdIntake(args) {
|
|
9793
|
+
const base = path18.join(profileDir(), "intake");
|
|
9794
|
+
const queueRoot = args.get("queue-dir") ?? path18.join(base, "queue");
|
|
9795
|
+
const doneRoot = args.get("done-dir") ?? path18.join(base, "done");
|
|
9796
|
+
const overflowRoot = args.get("overflow-dir") ?? path18.join(base, "overflow");
|
|
9797
|
+
const stateDir = args.get("state-dir") ?? path18.join(base, "state");
|
|
9798
|
+
const peersFile = args.get("peers-file") ?? path18.join(base, "peers.json");
|
|
9799
|
+
const once = args.has("once");
|
|
9800
|
+
const intervalMs = intervalSeconds(args.get("interval")) * 1e3;
|
|
9801
|
+
const log = jsonLineLog();
|
|
9802
|
+
ensureRealDir(stateDir, 448);
|
|
9803
|
+
ensureRealDir(queueRoot, 488);
|
|
9804
|
+
ensureRealDir(doneRoot, 448);
|
|
9805
|
+
ensureRealDir(overflowRoot, 448);
|
|
9806
|
+
const release = acquireLock(path18.join(stateDir, LOCK_FILE));
|
|
9807
|
+
try {
|
|
9808
|
+
if (args.has("init-from-now")) {
|
|
9809
|
+
const profile = loadProfile();
|
|
9810
|
+
const node = await courierNode(profile);
|
|
9811
|
+
await initFromNow({ kv: node.kv, profile, doneRoot, now: Date.now, log }, args.has("force"));
|
|
9812
|
+
return;
|
|
9813
|
+
}
|
|
9814
|
+
const state = readState(path18.join(stateDir, STATE_FILE));
|
|
9815
|
+
let peers = loadPeers(peersFile);
|
|
9816
|
+
log("intake-start", { once, intervalSeconds: intervalMs / 1e3 });
|
|
9817
|
+
for (; ; ) {
|
|
9818
|
+
try {
|
|
9819
|
+
const profile = loadProfile();
|
|
9820
|
+
const node = await courierNode(profile);
|
|
9821
|
+
try {
|
|
9822
|
+
peers = loadPeers(peersFile);
|
|
9823
|
+
} catch (error) {
|
|
9824
|
+
log("intake-error", { code: errInfo(error).code ?? "bad-peers" });
|
|
9825
|
+
}
|
|
9826
|
+
const queued = await intakeCycle({
|
|
9827
|
+
kv: node.kv,
|
|
9828
|
+
profile,
|
|
9829
|
+
peers,
|
|
9830
|
+
queueRoot,
|
|
9831
|
+
doneRoot,
|
|
9832
|
+
overflowRoot,
|
|
9833
|
+
stateDir,
|
|
9834
|
+
state,
|
|
9835
|
+
now: Date.now,
|
|
9836
|
+
log
|
|
9837
|
+
});
|
|
9838
|
+
log("intake-cycle", { queued });
|
|
9839
|
+
} catch (error) {
|
|
9840
|
+
log("intake-error", { code: errInfo(error).code ?? "error" });
|
|
9841
|
+
if (once)
|
|
9842
|
+
throw error;
|
|
9843
|
+
}
|
|
9844
|
+
if (once)
|
|
9845
|
+
return;
|
|
9846
|
+
await new Promise((resolve) => {
|
|
9847
|
+
setTimeout(resolve, intervalMs);
|
|
9848
|
+
});
|
|
9849
|
+
}
|
|
9850
|
+
} finally {
|
|
9851
|
+
release();
|
|
9852
|
+
}
|
|
9853
|
+
}
|
|
9854
|
+
|
|
8979
9855
|
// ../courier/dist/consumed.js
|
|
8980
9856
|
function isKeyMissing(error) {
|
|
8981
9857
|
const status = String(error.status ?? error.meta?.status ?? "");
|
|
@@ -9011,7 +9887,7 @@ async function markConsumed(kv2, id, at = /* @__PURE__ */ new Date()) {
|
|
|
9011
9887
|
}
|
|
9012
9888
|
|
|
9013
9889
|
// ../courier/dist/approval.js
|
|
9014
|
-
var
|
|
9890
|
+
var DAY_MS4 = 24 * 3600 * 1e3;
|
|
9015
9891
|
var MAX_NOTE = 2e3;
|
|
9016
9892
|
var FUTURE_SKEW_MS = 5 * 60 * 1e3;
|
|
9017
9893
|
function fail3(code, message) {
|
|
@@ -9065,7 +9941,7 @@ function verifyApproval(id, request, approval, options = {}) {
|
|
|
9065
9941
|
fail3("approval-mismatch", "The decision is dated in the future.");
|
|
9066
9942
|
if (decidedAt >= expires)
|
|
9067
9943
|
fail3("approval-expired", "The decision came after the request expired.");
|
|
9068
|
-
if (now - decidedAt >=
|
|
9944
|
+
if (now - decidedAt >= DAY_MS4)
|
|
9069
9945
|
fail3("approval-expired", "The approval is older than 24 hours; ask your owner again.");
|
|
9070
9946
|
if (typeof request.peer !== "string" || typeof request.threadId !== "string" || typeof request.taskId !== "string") {
|
|
9071
9947
|
fail3("approval-mismatch", "The request is missing peer, threadId or taskId.");
|
|
@@ -9163,9 +10039,9 @@ ${targetHelp()}`);
|
|
|
9163
10039
|
|
|
9164
10040
|
// ../courier/dist/broker.js
|
|
9165
10041
|
import { execFile } from "child_process";
|
|
9166
|
-
import
|
|
10042
|
+
import fs17 from "fs";
|
|
9167
10043
|
import os8 from "os";
|
|
9168
|
-
import
|
|
10044
|
+
import path19 from "path";
|
|
9169
10045
|
var MAX_OUTPUT = 4e3;
|
|
9170
10046
|
var EXEC_TIMEOUT_MS = 12e4;
|
|
9171
10047
|
var TOKEN = /^(gh[pousr]_[A-Za-z0-9]{20,255}|github_pat_[A-Za-z0-9_]{20,255})$/;
|
|
@@ -9177,7 +10053,7 @@ async function loadBrokerCredential(config, exec = execFileRunner) {
|
|
|
9177
10053
|
const result = await exec(config.ghBin, ["auth", "token", "--hostname", "github.com"], {
|
|
9178
10054
|
cwd: ownerHome,
|
|
9179
10055
|
timeoutMs: 1e4,
|
|
9180
|
-
env: { PATH: "/usr/bin:/bin", HOME: ownerHome, GH_CONFIG_DIR:
|
|
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" }
|
|
9181
10057
|
});
|
|
9182
10058
|
const token2 = result.stdout.trim();
|
|
9183
10059
|
if (result.code !== 0 || !validToken(token2))
|
|
@@ -9227,9 +10103,9 @@ var execFileRunner = (bin, args, { cwd, env, timeoutMs }) => {
|
|
|
9227
10103
|
};
|
|
9228
10104
|
function brokerEnv(credential, scratch) {
|
|
9229
10105
|
return {
|
|
9230
|
-
PATH: `${
|
|
10106
|
+
PATH: `${path19.dirname(credential.ghBin)}:/usr/bin:/bin`,
|
|
9231
10107
|
HOME: scratch,
|
|
9232
|
-
GH_CONFIG_DIR:
|
|
10108
|
+
GH_CONFIG_DIR: path19.join(scratch, "gh"),
|
|
9233
10109
|
GH_TOKEN: credential.token,
|
|
9234
10110
|
GH_PROMPT_DISABLED: "1",
|
|
9235
10111
|
GH_NO_UPDATE_NOTIFIER: "1",
|
|
@@ -9261,8 +10137,8 @@ async function runApproved(id, deps) {
|
|
|
9261
10137
|
if (await isConsumed(deps.kv, id))
|
|
9262
10138
|
fail4("approval-used", `Approval ${id} was already used; an approval covers one action.`);
|
|
9263
10139
|
const scratchRoot = deps.scratchRoot ?? brokerRoot();
|
|
9264
|
-
|
|
9265
|
-
const scratch =
|
|
10140
|
+
fs17.mkdirSync(scratchRoot, { recursive: true, mode: 448 });
|
|
10141
|
+
const scratch = fs17.mkdtempSync(path19.join(scratchRoot, "run-"));
|
|
9266
10142
|
const env = brokerEnv(credential, scratch);
|
|
9267
10143
|
const run2 = (args) => deps.exec(credential.ghBin, args, { cwd: scratch, env, timeoutMs: EXEC_TIMEOUT_MS });
|
|
9268
10144
|
const clean = (text) => sanitizeOutput(text, [credential.token]);
|
|
@@ -9299,7 +10175,7 @@ ${merged.stderr}` : ""}`).trim(),
|
|
|
9299
10175
|
...ok2 ? {} : { note: "The approval is used up. If the merge should still happen, file a new request." }
|
|
9300
10176
|
};
|
|
9301
10177
|
} finally {
|
|
9302
|
-
|
|
10178
|
+
fs17.rmSync(scratch, { recursive: true, force: true });
|
|
9303
10179
|
}
|
|
9304
10180
|
}
|
|
9305
10181
|
var runApprovedUsage = () => `Usage: ${cmd("run-approved <requestId>")}`;
|
|
@@ -9311,7 +10187,7 @@ async function cmdRunApproved(args) {
|
|
|
9311
10187
|
const config = loadBrokerConfig(currentProfileId(), testOwnerRoot);
|
|
9312
10188
|
assertPinnedProfile(config, loadProfile());
|
|
9313
10189
|
const node = await courierNode(loadProfile(config.courierHome), config.courierHome);
|
|
9314
|
-
const result = await runApproved(id, { kv: node.kv, credential: () => loadBrokerCredential(config), scratchRoot:
|
|
10190
|
+
const result = await runApproved(id, { kv: node.kv, credential: () => loadBrokerCredential(config), scratchRoot: path19.dirname(brokerFile(currentProfileId(), testOwnerRoot)), exec: execFileRunner, now: () => Date.now() });
|
|
9315
10191
|
await writeOut(`${JSON.stringify(result, null, 2)}
|
|
9316
10192
|
`);
|
|
9317
10193
|
if (!result.ok)
|
|
@@ -9339,7 +10215,7 @@ async function cmdBroker(args) {
|
|
|
9339
10215
|
return;
|
|
9340
10216
|
}
|
|
9341
10217
|
if (sub2 === "clear") {
|
|
9342
|
-
|
|
10218
|
+
fs17.rmSync(file, { force: true });
|
|
9343
10219
|
await writeOut('{"configured":false}\n');
|
|
9344
10220
|
return;
|
|
9345
10221
|
}
|
|
@@ -9349,33 +10225,33 @@ async function cmdBroker(args) {
|
|
|
9349
10225
|
if (ghBin === "true")
|
|
9350
10226
|
fail4("usage", "--gh-bin needs a path.");
|
|
9351
10227
|
if (ghBin === void 0) {
|
|
9352
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
9353
|
-
const candidate =
|
|
10228
|
+
for (const dir of (process.env.PATH ?? "").split(path19.delimiter)) {
|
|
10229
|
+
const candidate = path19.join(dir || ".", "gh");
|
|
9354
10230
|
try {
|
|
9355
|
-
|
|
9356
|
-
ghBin =
|
|
10231
|
+
fs17.accessSync(candidate, fs17.constants.X_OK);
|
|
10232
|
+
ghBin = fs17.realpathSync(candidate);
|
|
9357
10233
|
break;
|
|
9358
10234
|
} catch {
|
|
9359
10235
|
}
|
|
9360
10236
|
}
|
|
9361
10237
|
}
|
|
9362
|
-
if (!ghBin || !
|
|
10238
|
+
if (!ghBin || !path19.isAbsolute(ghBin))
|
|
9363
10239
|
fail4("usage", "Could not find gh; pass --gh-bin <absolute path>.");
|
|
9364
10240
|
try {
|
|
9365
|
-
|
|
10241
|
+
fs17.accessSync(ghBin, fs17.constants.X_OK);
|
|
9366
10242
|
} catch {
|
|
9367
10243
|
fail4("usage", `${ghBin} is not an executable file.`);
|
|
9368
10244
|
}
|
|
9369
10245
|
const profile = loadProfile();
|
|
9370
10246
|
let testToken;
|
|
9371
10247
|
if (testOwnerRoot !== void 0) {
|
|
9372
|
-
testToken =
|
|
10248
|
+
testToken = fs17.readFileSync(0, "utf8").trim();
|
|
9373
10249
|
if (testToken !== TEST_TOKEN)
|
|
9374
10250
|
fail4("usage", "Tests accept only the fixed dummy token fixture on stdin");
|
|
9375
10251
|
}
|
|
9376
|
-
ghBin =
|
|
9377
|
-
const st =
|
|
9378
|
-
const config = { v: 2, courierHome:
|
|
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 } : {} };
|
|
9379
10255
|
if (!testToken)
|
|
9380
10256
|
await loadBrokerCredential(config);
|
|
9381
10257
|
saveBrokerConfig(config, testOwnerRoot);
|
|
@@ -9470,6 +10346,9 @@ Delivery (one watcher per person per machine; never calls a model):
|
|
|
9470
10346
|
watch \u2026 --install-launchd | --install-systemd [--dry-run] [--name n]
|
|
9471
10347
|
Install the same watch command as a macOS launchd agent or a Linux
|
|
9472
10348
|
systemd --user unit. --dry-run prints the exact file and commands.
|
|
10349
|
+
intake [--queue-dir d] [--peers-file f] [--state-dir d] [--interval 60] [--once]
|
|
10350
|
+
Intake reader: copy paired peers' mirrored messages into the local queue
|
|
10351
|
+
(one <msgId>.json per message) with cheap checks. Never calls a model.
|
|
9473
10352
|
|
|
9474
10353
|
pause <peer> <threadId> [--reason "\u2026"] Humans: stop waking your agent for a thread; tells the peer
|
|
9475
10354
|
resume <peer> <threadId> Humans: wake it again (the wake budget starts over)
|
|
@@ -9518,6 +10397,8 @@ async function run(argv) {
|
|
|
9518
10397
|
return cmdOrders(sub2);
|
|
9519
10398
|
case "watch":
|
|
9520
10399
|
return cmdWatch(sub2);
|
|
10400
|
+
case "intake":
|
|
10401
|
+
return cmdIntake(sub2);
|
|
9521
10402
|
case "approval":
|
|
9522
10403
|
return cmdApproval(sub2);
|
|
9523
10404
|
case "pause":
|