@scp-dev/broker 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +45 -114
- package/dist/index.js +200 -529
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +8 -1
- package/package.json +3 -3
- package/src/bridge.ts +19 -3
- package/src/client.ts +2 -0
- package/src/index.ts +4 -2
- package/src/judge-gate.ts +14 -545
- package/src/server.ts +111 -24
- package/src/sessions.ts +39 -0
- package/src/testing.ts +10 -1
package/dist/index.js
CHANGED
|
@@ -681,6 +681,7 @@ function startBridge(options) {
|
|
|
681
681
|
const hello = await request("hello", { protocol: PROTOCOL2, machine, bridge: true });
|
|
682
682
|
account = hello.account;
|
|
683
683
|
core.setAliases([account]);
|
|
684
|
+
core.setOthers(account, Array.isArray(hello.others) ? hello.others.filter((name) => typeof name === "string") : []);
|
|
684
685
|
ready = true;
|
|
685
686
|
applyRemote(hello.endpoints, true);
|
|
686
687
|
await Promise.all(fullMirror().map((params) => request("mirror", params)));
|
|
@@ -763,7 +764,7 @@ function startBridge(options) {
|
|
|
763
764
|
function localAddress(address) {
|
|
764
765
|
if (typeof address !== "string" || !account || !address.startsWith(`${account}/`)) return address;
|
|
765
766
|
const rest = address.slice(account.length + 1);
|
|
766
|
-
return rest.includes("/") ?
|
|
767
|
+
return rest.includes("/") ? core.remoteAddress(rest) : `${core.user}/${account}/${rest}`;
|
|
767
768
|
}
|
|
768
769
|
function cursor() {
|
|
769
770
|
return inflight.size ? Math.min(...inflight) - 1 : ackedMax;
|
|
@@ -796,6 +797,7 @@ function startBridge(options) {
|
|
|
796
797
|
}
|
|
797
798
|
case "endpoints": {
|
|
798
799
|
if (typeof params.machine !== "string" || params.machine === machine) return;
|
|
800
|
+
if (typeof params.own === "boolean") core.setOther(params.machine, !params.own);
|
|
799
801
|
try {
|
|
800
802
|
core.mirror(params.machine, params.endpoints, { full: params.full === true });
|
|
801
803
|
} catch (error) {
|
|
@@ -987,6 +989,17 @@ function startBridge(options) {
|
|
|
987
989
|
}
|
|
988
990
|
if (mirrored(view)) fireAndForget("mirror", { endpoints: [outward(view)], full: false });
|
|
989
991
|
}
|
|
992
|
+
function renamed(endpoint, was) {
|
|
993
|
+
if (!ready) return;
|
|
994
|
+
let view;
|
|
995
|
+
try {
|
|
996
|
+
view = core.whoami(endpoint);
|
|
997
|
+
} catch {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (mirrored(view)) fireAndForget("mirror", { endpoints: [{ ...outward(view), was }], full: false });
|
|
1001
|
+
else resync();
|
|
1002
|
+
}
|
|
990
1003
|
function resync() {
|
|
991
1004
|
if (!ready) return;
|
|
992
1005
|
let frames;
|
|
@@ -1034,24 +1047,24 @@ function startBridge(options) {
|
|
|
1034
1047
|
if (socket && socket.readyState <= 1) socket.close(1e3, "the broker stopped");
|
|
1035
1048
|
}
|
|
1036
1049
|
connect();
|
|
1037
|
-
return { forward, changed, resync, activity, contacts, status, relink, stop };
|
|
1050
|
+
return { forward, changed, resync, renamed, activity, contacts, status, relink, stop };
|
|
1038
1051
|
}
|
|
1039
1052
|
|
|
1040
1053
|
// src/server.ts
|
|
1041
|
-
import { createHash as
|
|
1042
|
-
import { chmodSync as chmodSync2, existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as
|
|
1054
|
+
import { createHash as createHash3 } from "crypto";
|
|
1055
|
+
import { chmodSync as chmodSync2, existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync6, realpathSync, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
|
|
1043
1056
|
import { createServer } from "net";
|
|
1044
1057
|
import { homedir as homedir2 } from "os";
|
|
1045
|
-
import { dirname as dirname2, isAbsolute, join as
|
|
1058
|
+
import { dirname as dirname2, isAbsolute, join as join4, sep } from "path";
|
|
1046
1059
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
1047
1060
|
import {
|
|
1048
1061
|
BROKER_CAPABILITIES,
|
|
1049
1062
|
CLI_NAME,
|
|
1050
|
-
LIMITS
|
|
1063
|
+
LIMITS,
|
|
1051
1064
|
parsePolicy,
|
|
1052
1065
|
PREAMBLE,
|
|
1053
1066
|
PROTOCOL as PROTOCOL3,
|
|
1054
|
-
RelayError as
|
|
1067
|
+
RelayError as RelayError3,
|
|
1055
1068
|
SERVER_FIELDS,
|
|
1056
1069
|
stripControl
|
|
1057
1070
|
} from "@scp-dev/core";
|
|
@@ -1059,459 +1072,47 @@ import { FederatedCore } from "@scp-dev/hub";
|
|
|
1059
1072
|
|
|
1060
1073
|
// src/judge-gate.ts
|
|
1061
1074
|
import { accessSync, constants, readFileSync as readFileSync4 } from "fs";
|
|
1062
|
-
import {
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
judgeBoundary,
|
|
1069
|
-
LIMITS,
|
|
1070
|
-
mergeVerdicts,
|
|
1071
|
-
newId as newId2,
|
|
1072
|
-
newJudgeStats,
|
|
1073
|
-
one,
|
|
1074
|
-
RelayError as RelayError3,
|
|
1075
|
-
runJudge,
|
|
1076
|
-
sha256Hex,
|
|
1077
|
-
VerdictCache
|
|
1078
|
-
} from "@scp-dev/core";
|
|
1079
|
-
var HELD_PER_SENDER = 100;
|
|
1080
|
-
var iso = (ms) => ms === null ? null : new Date(ms).toISOString();
|
|
1081
|
-
var JudgeGate = class {
|
|
1082
|
-
constructor(o) {
|
|
1083
|
-
this.o = o;
|
|
1084
|
-
o.sql.all(
|
|
1085
|
-
`CREATE TABLE IF NOT EXISTS judge_holds (id TEXT PRIMARY KEY, from_ep TEXT NOT NULL, params TEXT NOT NULL, origin TEXT NOT NULL, verdict TEXT NOT NULL,
|
|
1086
|
-
created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, state TEXT NOT NULL, by TEXT, resolved_at INTEGER)`
|
|
1087
|
-
);
|
|
1088
|
-
o.sql.all(
|
|
1089
|
-
`CREATE TABLE IF NOT EXISTS judge_sent (from_ep TEXT NOT NULL, client_id TEXT NOT NULL, fingerprint TEXT NOT NULL, result TEXT NOT NULL, created_at INTEGER NOT NULL,
|
|
1090
|
-
PRIMARY KEY (from_ep, client_id))`
|
|
1091
|
-
);
|
|
1092
|
-
o.sql.all(`UPDATE judge_holds SET state = 'held' WHERE state = 'releasing'`);
|
|
1093
|
-
}
|
|
1094
|
-
o;
|
|
1095
|
-
stats = newJudgeStats();
|
|
1096
|
-
policy;
|
|
1097
|
-
version = "";
|
|
1098
|
-
client = null;
|
|
1099
|
-
cache = new VerdictCache();
|
|
1100
|
-
shadows = /* @__PURE__ */ new Set();
|
|
1101
|
-
/** judged sends in the last hour per budget: `ep:<id>`, `tree:<root id>` and `*` for the whole broker */
|
|
1102
|
-
spent = /* @__PURE__ */ new Map();
|
|
1103
|
-
/** the policy's judge section; a model client only when its key is readable, else the prefilter alone */
|
|
1104
|
-
configure(policy) {
|
|
1105
|
-
this.policy = policy;
|
|
1106
|
-
this.version = policy ? sha256Hex(JSON.stringify(policy)) : "";
|
|
1107
|
-
this.client = null;
|
|
1108
|
-
const model = policy?.model;
|
|
1109
|
-
if (!policy || policy.mode === "off" || !model) return;
|
|
1110
|
-
const key = this.keyReader(model);
|
|
1111
|
-
if (!key) {
|
|
1112
|
-
this.o.log(
|
|
1113
|
-
`relay broker: the judge model's key (${model.key_env ? `key_env ${model.key_env}` : `key_file ${model.key_file}`}) is not readable; judging with the prefilter only`
|
|
1114
|
-
);
|
|
1115
|
-
return;
|
|
1116
|
-
}
|
|
1117
|
-
this.client = (this.o.client ?? ((m, k) => new HttpJudgeClient({ model: m.id, effort: m.effort, key: k })))(model, key);
|
|
1118
|
-
}
|
|
1119
|
-
keyReader(model) {
|
|
1120
|
-
if (model.key_env) {
|
|
1121
|
-
const name = model.key_env;
|
|
1122
|
-
return this.o.env[name] ? async () => this.o.env[name] ?? "" : null;
|
|
1123
|
-
}
|
|
1124
|
-
const file = model.key_file;
|
|
1125
|
-
try {
|
|
1126
|
-
accessSync(file, constants.R_OK);
|
|
1127
|
-
} catch {
|
|
1128
|
-
return null;
|
|
1129
|
-
}
|
|
1130
|
-
return async () => readFileSync4(file, "utf8").trim();
|
|
1131
|
-
}
|
|
1132
|
-
/** resolves once every shadow judgement started so far finished */
|
|
1133
|
-
async settled() {
|
|
1134
|
-
await Promise.all([...this.shadows]);
|
|
1135
|
-
}
|
|
1136
|
-
/** a send or ask through the judge; an unjudged one goes straight to `deliver` */
|
|
1137
|
-
async send(from, params, origin, deliver) {
|
|
1138
|
-
const policy = this.policy;
|
|
1139
|
-
const body = params.body;
|
|
1140
|
-
if (!policy || policy.mode === "off" || typeof body !== "string" || new TextEncoder().encode(body).length > LIMITS.bodyBytes || findSecret2(body))
|
|
1141
|
-
return deliver(from, params, origin);
|
|
1142
|
-
const targets = Array.isArray(params.to) ? params.to : [params.to];
|
|
1143
|
-
if (targets.some((to) => this.o.core.foreignAccount(to) !== null)) {
|
|
1144
|
-
if (policy.mode === "enforce") {
|
|
1145
|
-
this.o.core.auditJudge("judge_skipped", from, null, {
|
|
1146
|
-
side: "local",
|
|
1147
|
-
boundary: "other account",
|
|
1148
|
-
prompt_version: JUDGE_PROMPT_VERSION,
|
|
1149
|
-
categories: [],
|
|
1150
|
-
sha256: sha256Hex(body),
|
|
1151
|
-
verdict: "skipped",
|
|
1152
|
-
reason: "other_account_judged_by_hub",
|
|
1153
|
-
to: targets.map(String)
|
|
1154
|
-
});
|
|
1155
|
-
}
|
|
1156
|
-
return deliver(from, params, origin);
|
|
1157
|
-
}
|
|
1158
|
-
if (policy.mode === "shadow") {
|
|
1159
|
-
const result = await deliver(from, params, origin);
|
|
1160
|
-
if (!result.duplicate) this.shadow(policy, from, params, origin, result.id);
|
|
1161
|
-
return result;
|
|
1162
|
-
}
|
|
1163
|
-
const { groups, tree } = this.boundaries(policy, from, params, origin);
|
|
1164
|
-
if (!groups.size) return deliver(from, params, origin);
|
|
1165
|
-
const ttl = params.ttl_s ?? LIMITS.ttlDefaultS;
|
|
1166
|
-
if (!Number.isInteger(ttl) || ttl < 1 || ttl > LIMITS.ttlMaxS) throw new RelayError3("invalid_params", `ttl_s must be 1 to ${LIMITS.ttlMaxS}`);
|
|
1167
|
-
const seen = params.client_id === void 0 ? void 0 : this.seen(from, params);
|
|
1168
|
-
if (seen) return seen;
|
|
1169
|
-
const waiting = one(this.o.sql, `SELECT COUNT(*) AS n FROM judge_holds WHERE from_ep = ? AND state IN ('held', 'releasing')`, from).n;
|
|
1170
|
-
if (waiting >= HELD_PER_SENDER)
|
|
1171
|
-
throw new RelayError3("rate_limited", `${waiting} of your messages wait for review by your human; send more once they released or refused some`, 3600);
|
|
1172
|
-
this.spend(policy, from, tree);
|
|
1173
|
-
const verdict = await this.judge(policy, from, body, groups);
|
|
1174
|
-
const detail = this.detail(verdict, body, groups);
|
|
1175
|
-
switch (verdict.verdict) {
|
|
1176
|
-
case "refuse":
|
|
1177
|
-
this.o.core.auditJudge("judge_refuse", from, null, detail);
|
|
1178
|
-
throw new RelayError3(
|
|
1179
|
-
"judge_refused",
|
|
1180
|
-
`the relay judge refused this message (local; ${verdict.categories.join(", ") || "no category"}): ${verdict.reason}`,
|
|
1181
|
-
void 0,
|
|
1182
|
-
{
|
|
1183
|
-
side: "local",
|
|
1184
|
-
categories: verdict.categories,
|
|
1185
|
-
reason: verdict.reason
|
|
1186
|
-
}
|
|
1187
|
-
);
|
|
1188
|
-
case "hold":
|
|
1189
|
-
return this.hold(from, params, origin, verdict, groups, detail);
|
|
1190
|
-
case "redact": {
|
|
1191
|
-
const applied = applyRedactions(body, verdict.redactions);
|
|
1192
|
-
const result = await deliver(from, { ...params, body: applied.text }, origin);
|
|
1193
|
-
const redacted = { n: applied.n, kinds: applied.kinds };
|
|
1194
|
-
this.o.core.auditJudge(verdict.source === "error" ? "judge_skipped" : "judge_redact", from, result.id, { ...detail, redacted });
|
|
1195
|
-
return this.remember(from, params, { ...result, redacted });
|
|
1196
|
-
}
|
|
1197
|
-
case "pass": {
|
|
1198
|
-
const result = await deliver(from, params, origin);
|
|
1199
|
-
if (verdict.source === "error") this.o.core.auditJudge("judge_skipped", from, result.id, detail);
|
|
1200
|
-
return this.remember(from, params, result);
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
/** an earlier judged send or waiting hold under the send's client_id: the same message is answered as before, another one refused as core refuses it */
|
|
1205
|
-
seen(from, params) {
|
|
1206
|
-
const clientId = params.client_id;
|
|
1207
|
-
const held2 = one(
|
|
1208
|
-
this.o.sql,
|
|
1209
|
-
`SELECT id, params FROM judge_holds WHERE from_ep = ? AND json_extract(params, '$.client_id') = ? AND state IN ('held', 'releasing')`,
|
|
1210
|
-
from,
|
|
1211
|
-
clientId
|
|
1212
|
-
);
|
|
1213
|
-
const sent = held2 ? void 0 : one(
|
|
1214
|
-
this.o.sql,
|
|
1215
|
-
`SELECT fingerprint, result FROM judge_sent WHERE from_ep = ? AND client_id = ? AND created_at > ?`,
|
|
1216
|
-
from,
|
|
1217
|
-
clientId,
|
|
1218
|
-
this.o.now() - LIMITS.idempotencyS * 1e3
|
|
1219
|
-
);
|
|
1220
|
-
const before = held2 ? fingerprint(JSON.parse(held2.params)) : sent?.fingerprint;
|
|
1221
|
-
if (before === void 0) return void 0;
|
|
1222
|
-
if (before !== fingerprint(params)) throw new RelayError3("invalid_params", `client_id ${clientId} was already used for a different message`);
|
|
1223
|
-
return held2 ? heldResult(held2.id, params) : { ...JSON.parse(sent.result), duplicate: true };
|
|
1224
|
-
}
|
|
1225
|
-
remember(from, params, result) {
|
|
1226
|
-
if (params.client_id !== void 0 && !result.duplicate) {
|
|
1227
|
-
this.o.sql.all(
|
|
1228
|
-
`INSERT OR REPLACE INTO judge_sent (from_ep, client_id, fingerprint, result, created_at) VALUES (?, ?, ?, ?, ?)`,
|
|
1229
|
-
from,
|
|
1230
|
-
params.client_id,
|
|
1231
|
-
fingerprint(params),
|
|
1232
|
-
JSON.stringify(result),
|
|
1233
|
-
this.o.now()
|
|
1234
|
-
);
|
|
1235
|
-
}
|
|
1236
|
-
return result;
|
|
1237
|
-
}
|
|
1238
|
-
/**
|
|
1239
|
-
* a judged send counts before any model call against its sender's, its lineage tree's and the whole broker's budget, since refused and
|
|
1240
|
-
* held sends never reach the send's own limits, and a new handle or a helper process must not bring a fresh budget
|
|
1241
|
-
*/
|
|
1242
|
-
spend(policy, from, tree) {
|
|
1243
|
-
const now = this.o.now();
|
|
1244
|
-
const global = policy.budget ?? DEFAULT_JUDGE_BUDGET;
|
|
1245
|
-
const budgets = [
|
|
1246
|
-
[`ep:${from}`, LIMITS.pairPerMinute, LIMITS.pairPerHour, "from this endpoint"],
|
|
1247
|
-
[`tree:${tree}`, LIMITS.pairPerMinute * 2, LIMITS.pairPerHour * 2, "from this lineage tree"],
|
|
1248
|
-
["*", global.per_minute, global.per_hour, "on this broker"]
|
|
1249
|
-
];
|
|
1250
|
-
const recent = budgets.map(([key]) => (this.spent.get(key) ?? []).filter((at) => at > now - 36e5));
|
|
1251
|
-
budgets.forEach(([key, perMinute, perHour, what], i) => {
|
|
1252
|
-
for (const [limit, windowS] of [
|
|
1253
|
-
[perMinute, 60],
|
|
1254
|
-
[perHour, 3600]
|
|
1255
|
-
]) {
|
|
1256
|
-
const inWindow = recent[i].filter((at) => at > now - windowS * 1e3);
|
|
1257
|
-
if (inWindow.length >= limit) {
|
|
1258
|
-
this.spent.set(key, recent[i]);
|
|
1259
|
-
throw new RelayError3(
|
|
1260
|
-
"rate_limited",
|
|
1261
|
-
`${limit} judged messages ${what} in ${windowS} s`,
|
|
1262
|
-
Math.max(1, Math.ceil((inWindow[0] + windowS * 1e3 - now) / 1e3))
|
|
1263
|
-
);
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
});
|
|
1267
|
-
for (const [i, [key]] of budgets.entries()) this.spent.set(key, [...recent[i], now]);
|
|
1268
|
-
}
|
|
1269
|
-
/** how many budgets are tracked (idle ones are evicted by the sweep) */
|
|
1270
|
-
budgetKeys() {
|
|
1271
|
-
return this.spent.size;
|
|
1272
|
-
}
|
|
1273
|
-
/** the held messages, newest first, without bodies */
|
|
1274
|
-
holds(state) {
|
|
1275
|
-
const rows = state ? this.o.sql.all(`SELECT * FROM judge_holds WHERE state = ? ORDER BY created_at DESC, rowid DESC LIMIT 200`, state) : this.o.sql.all(`SELECT * FROM judge_holds ORDER BY created_at DESC, rowid DESC LIMIT 200`);
|
|
1276
|
-
return rows.map((row) => this.view(row));
|
|
1277
|
-
}
|
|
1278
|
-
/** one hold with its body and the judge's redactions while it waits */
|
|
1279
|
-
show(id) {
|
|
1280
|
-
const row = this.row(id);
|
|
1281
|
-
const params = JSON.parse(row.params);
|
|
1282
|
-
const verdict = JSON.parse(row.verdict);
|
|
1283
|
-
return { ...this.view(row), ...typeof params.body === "string" ? { body: params.body } : {}, redactions: verdict.redactions };
|
|
1284
|
-
}
|
|
1285
|
-
/** delivers a held message as its sender, as stored or redacted: scope, policy and limits are checked again by the send itself */
|
|
1286
|
-
async release(id, options, deliver) {
|
|
1287
|
-
const row = one(
|
|
1288
|
-
this.o.sql,
|
|
1289
|
-
`UPDATE judge_holds SET state = 'releasing' WHERE id = ? AND state = 'held' AND expires_at > ? RETURNING *`,
|
|
1290
|
-
id,
|
|
1291
|
-
this.o.now()
|
|
1292
|
-
);
|
|
1293
|
-
if (!row) throw new RelayError3("not_found", `no held message ${id}`);
|
|
1294
|
-
const params = JSON.parse(row.params);
|
|
1295
|
-
const verdict = JSON.parse(row.verdict);
|
|
1296
|
-
let sent = params;
|
|
1297
|
-
let redacted;
|
|
1298
|
-
if (options.redacted) {
|
|
1299
|
-
const applied = applyRedactions(params.body, verdict.redactions);
|
|
1300
|
-
if (!applied.n) {
|
|
1301
|
-
this.o.sql.all(`UPDATE judge_holds SET state = 'held' WHERE id = ?`, id);
|
|
1302
|
-
throw new RelayError3("invalid_params", `hold ${id} has no redactions: release it as it is or refuse it`);
|
|
1303
|
-
}
|
|
1304
|
-
sent = { ...params, body: applied.text };
|
|
1305
|
-
redacted = { n: applied.n, kinds: applied.kinds };
|
|
1306
|
-
}
|
|
1307
|
-
let result;
|
|
1308
|
-
try {
|
|
1309
|
-
result = await deliver(row.from_ep, sent, JSON.parse(row.origin));
|
|
1310
|
-
} catch (error) {
|
|
1311
|
-
this.o.sql.all(`UPDATE judge_holds SET state = 'held' WHERE id = ?`, id);
|
|
1312
|
-
throw error;
|
|
1313
|
-
}
|
|
1314
|
-
this.remember(row.from_ep, params, { ...result, ...redacted ? { redacted } : {} });
|
|
1315
|
-
this.resolve(row, "released", options.by);
|
|
1316
|
-
this.o.core.auditJudge("judge_override", row.from_ep, id, {
|
|
1317
|
-
...this.storedDetail(row, verdict),
|
|
1318
|
-
by: options.by,
|
|
1319
|
-
action: redacted ? "release_redacted" : "release",
|
|
1320
|
-
...redacted ? { redacted } : {}
|
|
1321
|
-
});
|
|
1322
|
-
this.tell(
|
|
1323
|
-
row.from_ep,
|
|
1324
|
-
`relay: your message held for review as ${id} was released by your human${redacted ? " with redactions" : ""} and delivered as ${result.id}`
|
|
1325
|
-
);
|
|
1326
|
-
return { ...result, ...redacted ? { redacted } : {} };
|
|
1327
|
-
}
|
|
1328
|
-
/** refuses a held message: it is never delivered, and its sender hears the human's reason */
|
|
1329
|
-
refuse(id, reason, by) {
|
|
1330
|
-
this.expire(this.o.now());
|
|
1331
|
-
const row = one(this.o.sql, `SELECT * FROM judge_holds WHERE id = ? AND state = 'held'`, id);
|
|
1332
|
-
if (!row) throw new RelayError3("not_found", `no held message ${id}`);
|
|
1333
|
-
const verdict = JSON.parse(row.verdict);
|
|
1334
|
-
this.resolve(row, "refused", by);
|
|
1335
|
-
this.o.core.auditJudge("judge_override", row.from_ep, id, { ...this.storedDetail(row, verdict), by, action: "refuse", reason });
|
|
1336
|
-
this.tell(row.from_ep, `relay: your message held for review as ${id} was refused by your human: ${reason}`);
|
|
1337
|
-
return this.view(this.row(id));
|
|
1338
|
-
}
|
|
1339
|
-
/** expires holds past their message TTL (body purged, sender told) and drops resolved ones past the audit window */
|
|
1340
|
-
sweep() {
|
|
1341
|
-
const now = this.o.now();
|
|
1342
|
-
this.expire(now);
|
|
1343
|
-
this.o.sql.all(`DELETE FROM judge_holds WHERE state IN ('released', 'refused', 'expired') AND resolved_at <= ?`, now - LIMITS.auditKeepS * 1e3);
|
|
1344
|
-
this.o.sql.all(`DELETE FROM judge_sent WHERE created_at <= ?`, now - LIMITS.idempotencyS * 1e3);
|
|
1345
|
-
for (const [key, times] of this.spent) if (!times.length || times[times.length - 1] <= now - 36e5) this.spent.delete(key);
|
|
1346
|
-
}
|
|
1347
|
-
expire(now) {
|
|
1348
|
-
for (const row of this.o.sql.all(`SELECT * FROM judge_holds WHERE state = 'held' AND expires_at <= ?`, now)) {
|
|
1349
|
-
const verdict = JSON.parse(row.verdict);
|
|
1350
|
-
this.resolve(row, "expired", null);
|
|
1351
|
-
this.o.core.auditJudge("judge_expire", row.from_ep, row.id, this.storedDetail(row, verdict));
|
|
1352
|
-
this.tell(row.from_ep, `relay: your message held for review as ${row.id} expired without review and was not delivered`);
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
/** the recipients by the boundary their message crosses; throws as send does for a target the sender cannot reach */
|
|
1356
|
-
boundaries(policy, from, params, origin) {
|
|
1357
|
-
const { sender, recipients, tree } = this.o.core.judgeSubjects(from, params, origin);
|
|
1358
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1359
|
-
for (const recipient of recipients) {
|
|
1360
|
-
const label = judgeBoundary(policy, sender, recipient);
|
|
1361
|
-
if (label !== null) groups.set(label, [...groups.get(label) ?? [], recipient.address]);
|
|
1362
|
-
}
|
|
1363
|
-
return { groups, tree };
|
|
1364
|
-
}
|
|
1365
|
-
async judge(policy, from, body, groups) {
|
|
1366
|
-
const verdicts = await Promise.all(
|
|
1367
|
-
[...groups].map(
|
|
1368
|
-
([boundary, to]) => runJudge({
|
|
1369
|
-
body,
|
|
1370
|
-
side: "local",
|
|
1371
|
-
boundary,
|
|
1372
|
-
configVersion: this.version,
|
|
1373
|
-
client: this.client,
|
|
1374
|
-
cache: this.cache,
|
|
1375
|
-
timeoutMs: policy.timeout_s * 1e3,
|
|
1376
|
-
failMode: policy.fail,
|
|
1377
|
-
...this.client && policy.model ? { model: policy.model.id } : {},
|
|
1378
|
-
from: this.o.core.addressOf(from),
|
|
1379
|
-
to,
|
|
1380
|
-
now: this.o.now,
|
|
1381
|
-
stats: this.stats
|
|
1382
|
-
})
|
|
1383
|
-
)
|
|
1384
|
-
);
|
|
1385
|
-
return mergeVerdicts(verdicts);
|
|
1386
|
-
}
|
|
1387
|
-
shadow(policy, from, params, origin, ref) {
|
|
1388
|
-
const task = (async () => {
|
|
1389
|
-
try {
|
|
1390
|
-
const { groups, tree } = this.boundaries(policy, from, params, origin);
|
|
1391
|
-
if (!groups.size) return;
|
|
1392
|
-
this.spend(policy, from, tree);
|
|
1393
|
-
const body = params.body;
|
|
1394
|
-
const verdict = await this.judge(policy, from, body, groups);
|
|
1395
|
-
this.o.core.auditJudge("judge_shadow", from, ref, this.detail(verdict, body, groups));
|
|
1396
|
-
} catch (error) {
|
|
1397
|
-
this.o.log(`relay broker: a shadow judgement failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1398
|
-
}
|
|
1399
|
-
})();
|
|
1400
|
-
this.shadows.add(task);
|
|
1401
|
-
void task.finally(() => this.shadows.delete(task));
|
|
1402
|
-
}
|
|
1403
|
-
hold(from, params, origin, verdict, groups, detail) {
|
|
1404
|
-
const now = this.o.now();
|
|
1405
|
-
const id = newId2("hold", now);
|
|
1406
|
-
const stored = { ...params, client_id: params.client_id ?? id };
|
|
1407
|
-
const to = [...groups.values()].flat();
|
|
1408
|
-
const record = { ...verdict, boundary: [...groups.keys()].join(" | "), to };
|
|
1409
|
-
this.o.sql.all(
|
|
1410
|
-
`INSERT INTO judge_holds (id, from_ep, params, origin, verdict, created_at, expires_at, state) VALUES (?, ?, ?, ?, ?, ?, ?, 'held')`,
|
|
1411
|
-
id,
|
|
1412
|
-
from,
|
|
1413
|
-
JSON.stringify(stored),
|
|
1414
|
-
JSON.stringify(origin),
|
|
1415
|
-
JSON.stringify(record),
|
|
1416
|
-
now,
|
|
1417
|
-
now + (params.ttl_s ?? LIMITS.ttlDefaultS) * 1e3
|
|
1418
|
-
);
|
|
1419
|
-
this.o.core.auditJudge("judge_hold", from, id, detail);
|
|
1420
|
-
return heldResult(id, params);
|
|
1421
|
-
}
|
|
1422
|
-
detail(verdict, body, groups) {
|
|
1423
|
-
return {
|
|
1424
|
-
side: "local",
|
|
1425
|
-
boundary: [...groups.keys()].join(" | "),
|
|
1426
|
-
prompt_version: verdict.prompt_version,
|
|
1427
|
-
categories: verdict.categories,
|
|
1428
|
-
sha256: sha256Hex(body),
|
|
1429
|
-
verdict: verdict.verdict,
|
|
1430
|
-
source: verdict.source,
|
|
1431
|
-
reason: verdict.reason,
|
|
1432
|
-
to: [...groups.values()].flat(),
|
|
1433
|
-
...verdict.model ? { model: verdict.model } : {},
|
|
1434
|
-
...verdict.ms !== void 0 ? { ms: verdict.ms } : {}
|
|
1435
|
-
};
|
|
1436
|
-
}
|
|
1437
|
-
storedDetail(row, verdict) {
|
|
1438
|
-
const params = JSON.parse(row.params);
|
|
1439
|
-
return {
|
|
1440
|
-
side: "local",
|
|
1441
|
-
boundary: verdict.boundary,
|
|
1442
|
-
prompt_version: verdict.prompt_version,
|
|
1443
|
-
categories: verdict.categories,
|
|
1444
|
-
sha256: params.sha256 ?? sha256Hex(params.body ?? ""),
|
|
1445
|
-
verdict: verdict.verdict,
|
|
1446
|
-
to: verdict.to,
|
|
1447
|
-
...verdict.model ? { model: verdict.model } : {}
|
|
1448
|
-
};
|
|
1075
|
+
import { JudgeGate as HubJudgeGate } from "@scp-dev/hub";
|
|
1076
|
+
function readKeyFile(file) {
|
|
1077
|
+
try {
|
|
1078
|
+
accessSync(file, constants.R_OK);
|
|
1079
|
+
} catch {
|
|
1080
|
+
return null;
|
|
1449
1081
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
const kinds = { ...verdict, redactions: verdict.redactions.map((r) => ({ kind: r.kind })) };
|
|
1456
|
-
this.o.sql.all(
|
|
1457
|
-
`UPDATE judge_holds SET state = ?, by = ?, resolved_at = ?, params = ?, verdict = ? WHERE id = ?`,
|
|
1458
|
-
state,
|
|
1459
|
-
by,
|
|
1460
|
-
this.o.now(),
|
|
1461
|
-
JSON.stringify(purged),
|
|
1462
|
-
JSON.stringify(kinds),
|
|
1463
|
-
row.id
|
|
1464
|
-
);
|
|
1465
|
-
}
|
|
1466
|
-
tell(endpoint, text) {
|
|
1467
|
-
try {
|
|
1468
|
-
this.o.core.judgeNotice(endpoint, text);
|
|
1469
|
-
this.o.wake(endpoint);
|
|
1470
|
-
} catch (error) {
|
|
1471
|
-
if (!(error instanceof RelayError3)) throw error;
|
|
1472
|
-
}
|
|
1082
|
+
return async () => readFileSync4(file, "utf8").trim();
|
|
1083
|
+
}
|
|
1084
|
+
var JudgeGate = class extends HubJudgeGate {
|
|
1085
|
+
constructor(options) {
|
|
1086
|
+
super({ keyFile: readKeyFile, ...options });
|
|
1473
1087
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
// src/sessions.ts
|
|
1091
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
1092
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, rmSync, writeFileSync } from "fs";
|
|
1093
|
+
import { join as join3 } from "path";
|
|
1094
|
+
function markerPath(dir, session) {
|
|
1095
|
+
return join3(dir, "sessions", `${createHash2("sha256").update(session).digest("hex").slice(0, 32)}.json`);
|
|
1096
|
+
}
|
|
1097
|
+
function readSessionMarker(dir, session) {
|
|
1098
|
+
try {
|
|
1099
|
+
const value = JSON.parse(readFileSync5(markerPath(dir, session), "utf8"));
|
|
1100
|
+
if (typeof value?.joined !== "boolean") return void 0;
|
|
1101
|
+
return { joined: value.joined, ...typeof value.handle === "string" ? { handle: value.handle } : {} };
|
|
1102
|
+
} catch {
|
|
1103
|
+
return void 0;
|
|
1478
1104
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
categories: verdict.categories,
|
|
1490
|
-
reason: verdict.reason,
|
|
1491
|
-
boundary: verdict.boundary,
|
|
1492
|
-
source: verdict.source,
|
|
1493
|
-
bytes: params.bytes ?? new TextEncoder().encode(params.body ?? "").length,
|
|
1494
|
-
by: row.by,
|
|
1495
|
-
resolved_at: iso(row.resolved_at)
|
|
1496
|
-
};
|
|
1105
|
+
}
|
|
1106
|
+
function writeSessionMarker(dir, session, marker) {
|
|
1107
|
+
const path = markerPath(dir, session);
|
|
1108
|
+
mkdirSync2(join3(dir, "sessions"), { recursive: true, mode: 448 });
|
|
1109
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
1110
|
+
try {
|
|
1111
|
+
writeFileSync(temp, JSON.stringify(marker), { mode: 384 });
|
|
1112
|
+
renameSync2(temp, path);
|
|
1113
|
+
} finally {
|
|
1114
|
+
rmSync(temp, { force: true });
|
|
1497
1115
|
}
|
|
1498
|
-
};
|
|
1499
|
-
var fingerprint = (p) => sha256Hex(
|
|
1500
|
-
JSON.stringify([
|
|
1501
|
-
[p.to].flat().map(String).sort(),
|
|
1502
|
-
p.body,
|
|
1503
|
-
p.kind ?? "message",
|
|
1504
|
-
p.thread ?? null,
|
|
1505
|
-
p.reply_to ?? null,
|
|
1506
|
-
p.data_class ?? "public",
|
|
1507
|
-
(p.attachments ?? []).map((a) => a?.path).sort(),
|
|
1508
|
-
p.mime ?? null,
|
|
1509
|
-
p.ttl_s ?? LIMITS.ttlDefaultS,
|
|
1510
|
-
p.require_reply ?? null
|
|
1511
|
-
])
|
|
1512
|
-
);
|
|
1513
|
-
function heldResult(id, params) {
|
|
1514
|
-
return { id, thread: params.thread ?? "", hop: 0, deliveries: [], held: { hold_id: id, side: "local" } };
|
|
1515
1116
|
}
|
|
1516
1117
|
|
|
1517
1118
|
// src/sqlite.ts
|
|
@@ -1609,7 +1210,7 @@ var SCOPE_METHODS = /* @__PURE__ */ new Set([
|
|
|
1609
1210
|
var CHANGES_SCOPE = /* @__PURE__ */ new Set(["rules_set", "rules_widen", "rules_lift", "group"]);
|
|
1610
1211
|
var ASK_RECHECK_MS = 1e3;
|
|
1611
1212
|
var STOP_RECHECK_MS = 1e3;
|
|
1612
|
-
var TRANSIENT_METHODS = /* @__PURE__ */ new Set(["event", "status", "whoami", "inject", "injected", "peek_turn", "stop_wait", "prompted"]);
|
|
1213
|
+
var TRANSIENT_METHODS = /* @__PURE__ */ new Set(["event", "status", "whoami", "inject", "injected", "peek_turn", "stop_wait", "prompted", "title_infer"]);
|
|
1613
1214
|
var GRANT_UNBOUND = `a spawn-env grant binds only a new harness process: run the harness with it, or name the child (${CLI_NAME} spawn-env --name / --as)`;
|
|
1614
1215
|
function workerResults(result) {
|
|
1615
1216
|
return result.messages.length - (result.others_shown ?? 0) + result.more;
|
|
@@ -1631,7 +1232,7 @@ function hookOf(s) {
|
|
|
1631
1232
|
}
|
|
1632
1233
|
async function startBroker(options) {
|
|
1633
1234
|
const dir = privateDir(options.dir);
|
|
1634
|
-
const lock = takeBrokerLock(
|
|
1235
|
+
const lock = takeBrokerLock(join4(dir, "broker.lock"));
|
|
1635
1236
|
const socket = socketPath(dir);
|
|
1636
1237
|
const connections = /* @__PURE__ */ new Set();
|
|
1637
1238
|
const waiters = /* @__PURE__ */ new Map();
|
|
@@ -1659,16 +1260,16 @@ async function startBroker(options) {
|
|
|
1659
1260
|
let listener;
|
|
1660
1261
|
try {
|
|
1661
1262
|
privateDir(dirname2(socket));
|
|
1662
|
-
db = openDatabase(
|
|
1263
|
+
db = openDatabase(join4(dir, "relay.db"));
|
|
1663
1264
|
db.exec("PRAGMA journal_mode = WAL");
|
|
1664
1265
|
db.exec("PRAGMA busy_timeout = 5000");
|
|
1665
|
-
chmodSync2(
|
|
1266
|
+
chmodSync2(join4(dir, "relay.db"), 384);
|
|
1666
1267
|
const sql = db;
|
|
1667
1268
|
const machine = options.machine ?? options.bridge?.machine;
|
|
1668
1269
|
core = new FederatedCore(sql, {
|
|
1669
1270
|
...compact({ user: options.user, machine, now: options.now }),
|
|
1670
1271
|
hostname: shortHostname(),
|
|
1671
|
-
hash: (value) =>
|
|
1272
|
+
hash: (value) => createHash3("sha256").update(value).digest("hex"),
|
|
1672
1273
|
transaction: (fn) => db.transaction(fn),
|
|
1673
1274
|
alive: running
|
|
1674
1275
|
});
|
|
@@ -1724,6 +1325,7 @@ async function startBroker(options) {
|
|
|
1724
1325
|
const known = /* @__PURE__ */ new Map();
|
|
1725
1326
|
for (const view of localViews()) known.set(view.id, view.presence);
|
|
1726
1327
|
const presenceTimer = setInterval(() => {
|
|
1328
|
+
core.endExited();
|
|
1727
1329
|
const live = /* @__PURE__ */ new Set();
|
|
1728
1330
|
for (const view of localViews()) {
|
|
1729
1331
|
live.add(view.id);
|
|
@@ -1731,21 +1333,17 @@ async function startBroker(options) {
|
|
|
1731
1333
|
}
|
|
1732
1334
|
for (const id of known.keys()) if (!live.has(id)) known.delete(id);
|
|
1733
1335
|
}, options.presenceEveryMs ?? PRESENCE_MS);
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
log
|
|
1746
|
-
}
|
|
1747
|
-
});
|
|
1748
|
-
}
|
|
1336
|
+
const remoteHooks = {
|
|
1337
|
+
wake,
|
|
1338
|
+
fleet: notifyFleet,
|
|
1339
|
+
policyError: () => {
|
|
1340
|
+
loadPolicy();
|
|
1341
|
+
return policyError;
|
|
1342
|
+
},
|
|
1343
|
+
log
|
|
1344
|
+
};
|
|
1345
|
+
if (options.bridge) bridge = startBridge({ ...options.bridge, core, onRemote: remoteHooks });
|
|
1346
|
+
else if (options.link) bridge = options.link({ core, hooks: remoteHooks, send: (from, params, origin) => judge.send(from, params, origin, deliver) });
|
|
1749
1347
|
async function relink() {
|
|
1750
1348
|
const config = options.bridgeConfig ? await options.bridgeConfig() : options.bridge;
|
|
1751
1349
|
if (!config || bridge && config.url === options.bridge?.url && config.machine === options.bridge?.machine) {
|
|
@@ -1763,7 +1361,7 @@ async function startBroker(options) {
|
|
|
1763
1361
|
function loadPolicy() {
|
|
1764
1362
|
let text;
|
|
1765
1363
|
try {
|
|
1766
|
-
text =
|
|
1364
|
+
text = readFileSync6(policyFile, "utf8");
|
|
1767
1365
|
} catch (error) {
|
|
1768
1366
|
const code = error.code;
|
|
1769
1367
|
const dangling = code === "ENOENT" && lstatSync2(policyFile, { throwIfNoEntry: false }) !== void 0;
|
|
@@ -1828,6 +1426,20 @@ async function startBroker(options) {
|
|
|
1828
1426
|
notifyFleet({ type: "presence", endpoint: view.address, presence: view.presence, harness: view.harness, labels: view.labels });
|
|
1829
1427
|
bridge?.changed(view.id);
|
|
1830
1428
|
}
|
|
1429
|
+
function membership(view, session, joined, was) {
|
|
1430
|
+
const handle = view.address.slice(view.address.lastIndexOf("/") + 1);
|
|
1431
|
+
if (session && !session.startsWith("sub:")) {
|
|
1432
|
+
try {
|
|
1433
|
+
writeSessionMarker(dir, session, { joined, handle });
|
|
1434
|
+
} catch (error) {
|
|
1435
|
+
log(`relay broker: the session marker was not written: ${error instanceof Error ? error.message : String(error)}`);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
known.set(view.id, view.presence);
|
|
1439
|
+
notifyFleet({ type: "presence", endpoint: view.address, presence: view.presence, harness: view.harness, labels: view.labels });
|
|
1440
|
+
if (was !== void 0 && was !== handle) bridge?.renamed(view.id, was);
|
|
1441
|
+
else bridge?.changed(view.id);
|
|
1442
|
+
}
|
|
1831
1443
|
function notifyFleet(event) {
|
|
1832
1444
|
const at = new Date((options.now ?? Date.now)()).toISOString();
|
|
1833
1445
|
let line;
|
|
@@ -1887,7 +1499,7 @@ async function startBroker(options) {
|
|
|
1887
1499
|
endpoint: core.whoami(endpoint),
|
|
1888
1500
|
capabilities: BROKER_CAPABILITIES,
|
|
1889
1501
|
server_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1890
|
-
limits:
|
|
1502
|
+
limits: LIMITS,
|
|
1891
1503
|
preamble: PREAMBLE,
|
|
1892
1504
|
broker: brokerInfo()
|
|
1893
1505
|
};
|
|
@@ -1917,16 +1529,16 @@ async function startBroker(options) {
|
|
|
1917
1529
|
}
|
|
1918
1530
|
async function gather(me, result, peek, s) {
|
|
1919
1531
|
const gathering = (r) => workerResults(r) > 0 && !r.more && !r.prompted && othersRunning(r) > 0;
|
|
1920
|
-
const cap = Date.now() +
|
|
1921
|
-
let quiet = Date.now() +
|
|
1532
|
+
const cap = Date.now() + LIMITS.pushGraceS * 1e3;
|
|
1533
|
+
let quiet = Date.now() + LIMITS.pushQuietS * 1e3;
|
|
1922
1534
|
let results = workerResults(result);
|
|
1923
1535
|
while (gathering(result) && !s.data.closed && Date.now() < Math.min(quiet, cap)) {
|
|
1924
1536
|
await waitFor(me, Math.min(quiet, cap) - Date.now(), s);
|
|
1925
|
-
if (closing) throw new
|
|
1537
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
1926
1538
|
result = core.stopWait(me, peek);
|
|
1927
1539
|
if (workerResults(result) > results) {
|
|
1928
1540
|
results = workerResults(result);
|
|
1929
|
-
quiet = Date.now() +
|
|
1541
|
+
quiet = Date.now() + LIMITS.pushQuietS * 1e3;
|
|
1930
1542
|
}
|
|
1931
1543
|
}
|
|
1932
1544
|
}
|
|
@@ -2020,7 +1632,7 @@ async function startBroker(options) {
|
|
|
2020
1632
|
if (hasId) write(s, { jsonrpc: "2.0", id, result });
|
|
2021
1633
|
} catch (error) {
|
|
2022
1634
|
if (!hasId || closing) return;
|
|
2023
|
-
if (error instanceof
|
|
1635
|
+
if (error instanceof RelayError3) {
|
|
2024
1636
|
const code = error.code === "method_not_found" ? -32601 : error.code === "invalid_params" ? -32602 : -32e3;
|
|
2025
1637
|
write(s, {
|
|
2026
1638
|
jsonrpc: "2.0",
|
|
@@ -2041,59 +1653,70 @@ async function startBroker(options) {
|
|
|
2041
1653
|
}
|
|
2042
1654
|
}
|
|
2043
1655
|
function endpointOf(s) {
|
|
2044
|
-
if (closing) throw new
|
|
1656
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
2045
1657
|
if (!s.data.endpoint)
|
|
2046
|
-
throw new
|
|
1658
|
+
throw new RelayError3("unauthorized", s.data.observer ? "this connection only observes: say hello as an agent or the human" : "call hello first");
|
|
2047
1659
|
core.touch(s.data.endpoint);
|
|
2048
1660
|
return s.data.endpoint;
|
|
2049
1661
|
}
|
|
2050
1662
|
function waitSeconds(value) {
|
|
2051
1663
|
if (value === void 0) return 0;
|
|
2052
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new
|
|
2053
|
-
return Math.min(value,
|
|
1664
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new RelayError3("invalid_params", "wait_s must be a number of seconds");
|
|
1665
|
+
return Math.min(value, LIMITS.waitMaxS) * 1e3;
|
|
2054
1666
|
}
|
|
2055
1667
|
function checkAttachments(attachments, s) {
|
|
2056
1668
|
if (attachments === void 0) return void 0;
|
|
2057
|
-
if (!Array.isArray(attachments)) throw new
|
|
1669
|
+
if (!Array.isArray(attachments)) throw new RelayError3("invalid_params", "attachments must be a list");
|
|
2058
1670
|
const tooWide = /* @__PURE__ */ new Set(["/", realOrSelf(homedir2()), realOrSelf(accountHome())]);
|
|
2059
1671
|
const roots = [s.data.cwd, ...options.roots ?? []].filter((r) => typeof r === "string" && isAbsolute(r)).map(realOrSelf).filter((r) => !tooWide.has(r));
|
|
2060
1672
|
return attachments.map((attachment) => {
|
|
2061
1673
|
if (attachment?.kind !== "path" || typeof attachment.path !== "string" || !isAbsolute(attachment.path)) {
|
|
2062
|
-
throw new
|
|
1674
|
+
throw new RelayError3("invalid_params", "an attachment is {kind: path, path: an absolute path}");
|
|
2063
1675
|
}
|
|
2064
1676
|
let real;
|
|
2065
1677
|
try {
|
|
2066
1678
|
real = realpathSync(attachment.path);
|
|
2067
1679
|
} catch {
|
|
2068
|
-
throw new
|
|
1680
|
+
throw new RelayError3("not_found", `no file at ${attachment.path}`);
|
|
2069
1681
|
}
|
|
2070
1682
|
if (!roots.some((root) => real === root || real.startsWith(root.endsWith(sep) ? root : root + sep))) {
|
|
2071
|
-
throw new
|
|
1683
|
+
throw new RelayError3("invalid_params", `${attachment.path} is outside the sender's working directory and the relay's allowed roots`);
|
|
2072
1684
|
}
|
|
2073
1685
|
return { kind: "path", path: real };
|
|
2074
1686
|
});
|
|
2075
1687
|
}
|
|
2076
1688
|
async function dispatch(s, method, params) {
|
|
2077
1689
|
if (method === "hello") {
|
|
2078
|
-
if (s.data.endpoint || s.data.observer) throw new
|
|
2079
|
-
if (params.protocol !== PROTOCOL3) throw new
|
|
1690
|
+
if (s.data.endpoint || s.data.observer) throw new RelayError3("invalid_params", "hello was already called on this connection");
|
|
1691
|
+
if (params.protocol !== PROTOCOL3) throw new RelayError3("invalid_params", `this broker speaks ${PROTOCOL3}`);
|
|
2080
1692
|
if (params.transient === true) {
|
|
2081
1693
|
if (params.human === true || params.observe === true || params.resume !== void 0) {
|
|
2082
|
-
throw new
|
|
1694
|
+
throw new RelayError3("invalid_params", "a hook connection claims an agent endpoint: transient does not go with human, observe or resume");
|
|
2083
1695
|
}
|
|
2084
|
-
const
|
|
2085
|
-
|
|
1696
|
+
const join5 = params.join;
|
|
1697
|
+
if (join5 !== void 0 && (typeof join5 !== "object" || join5 === null || Array.isArray(join5)))
|
|
1698
|
+
throw new RelayError3("invalid_params", "join is {handle?, under?}");
|
|
1699
|
+
const under = join5?.under;
|
|
1700
|
+
const before2 = new Map(core.list({ all: true }).map((view) => [view.id, view]));
|
|
1701
|
+
const { endpoint: endpoint2, subagent, created } = join5 === void 0 ? core.claim(params.claim ?? {}, { transient: true }) : {
|
|
1702
|
+
endpoint: core.joinSession(params.claim ?? {}, join5.handle, under),
|
|
1703
|
+
subagent: void 0,
|
|
1704
|
+
created: void 0
|
|
1705
|
+
};
|
|
2086
1706
|
s.data.endpoint = endpoint2.id;
|
|
2087
1707
|
s.data.transient = true;
|
|
2088
1708
|
const key = params.claim?.process;
|
|
2089
1709
|
if (typeof key === "string") s.data.process = key;
|
|
2090
|
-
|
|
1710
|
+
const was2 = before2.get(endpoint2.id)?.address;
|
|
1711
|
+
if (join5 !== void 0) membership(endpoint2, core.sessionOf(endpoint2.id), true, was2?.slice(was2.lastIndexOf("/") + 1));
|
|
1712
|
+
if (under !== void 0 && before2.get(endpoint2.id)?.parent !== endpoint2.parent) bridge?.resync();
|
|
1713
|
+
else if ((before2.get(endpoint2.id)?.presence ?? "offline") !== endpoint2.presence) presenceChanged(endpoint2);
|
|
2091
1714
|
return {
|
|
2092
1715
|
endpoint: endpoint2,
|
|
2093
1716
|
...subagent ? { subagent, ...created ? { created } : {} } : {},
|
|
2094
1717
|
capabilities: BROKER_CAPABILITIES,
|
|
2095
1718
|
server_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2096
|
-
limits:
|
|
1719
|
+
limits: LIMITS,
|
|
2097
1720
|
preamble: PREAMBLE,
|
|
2098
1721
|
broker: brokerInfo()
|
|
2099
1722
|
};
|
|
@@ -2101,7 +1724,7 @@ async function startBroker(options) {
|
|
|
2101
1724
|
if (params.observe === true) {
|
|
2102
1725
|
const self = params.self === void 0 ? null : core.placeLineage(params.self);
|
|
2103
1726
|
if (params.grant !== void 0) {
|
|
2104
|
-
if (!core.grantBinds(params.grant, self)) throw new
|
|
1727
|
+
if (!core.grantBinds(params.grant, self)) throw new RelayError3("unauthorized", GRANT_UNBOUND);
|
|
2105
1728
|
return { ...bindLineage(s, self, params, true), grant_bound: true };
|
|
2106
1729
|
}
|
|
2107
1730
|
if (self) return bindLineage(s, self, params, true);
|
|
@@ -2111,7 +1734,7 @@ async function startBroker(options) {
|
|
|
2111
1734
|
endpoint: null,
|
|
2112
1735
|
capabilities: BROKER_CAPABILITIES,
|
|
2113
1736
|
server_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2114
|
-
limits:
|
|
1737
|
+
limits: LIMITS,
|
|
2115
1738
|
preamble: PREAMBLE,
|
|
2116
1739
|
broker: brokerInfo()
|
|
2117
1740
|
};
|
|
@@ -2119,7 +1742,10 @@ async function startBroker(options) {
|
|
|
2119
1742
|
const claim = params.claim;
|
|
2120
1743
|
if (params.human === true && params.resume === void 0 && params.lineage !== void 0) {
|
|
2121
1744
|
const placed = core.placeLineage(params.lineage);
|
|
2122
|
-
if (placed)
|
|
1745
|
+
if (placed) {
|
|
1746
|
+
s.data.terminal = params.tty === true;
|
|
1747
|
+
return bindLineage(s, placed, params);
|
|
1748
|
+
}
|
|
2123
1749
|
} else if (params.human !== true && params.resume === void 0 && claim?.process === void 0 && typeof claim?.handle === "string") {
|
|
2124
1750
|
const placed = claim.lineage === void 0 ? null : core.placeLineage(claim.lineage, claim.handle);
|
|
2125
1751
|
if (placed && (claim.grant === void 0 || core.grantBinds(claim.grant, placed))) {
|
|
@@ -2154,17 +1780,17 @@ async function startBroker(options) {
|
|
|
2154
1780
|
...token ? { token } : {},
|
|
2155
1781
|
capabilities: BROKER_CAPABILITIES,
|
|
2156
1782
|
server_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2157
|
-
limits:
|
|
1783
|
+
limits: LIMITS,
|
|
2158
1784
|
preamble: PREAMBLE,
|
|
2159
1785
|
broker: brokerInfo()
|
|
2160
1786
|
};
|
|
2161
1787
|
}
|
|
2162
|
-
if (closing) throw new
|
|
1788
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
2163
1789
|
if (s.data.observer && OBSERVER_METHODS.has(method)) return observe(s, method, params);
|
|
2164
1790
|
if (s.data.observer && SCOPE_METHODS.has(method) && (s.data.lineage || method === "why")) return scope(s, method, params);
|
|
2165
|
-
if (s.data.transient && SCOPE_METHODS.has(method)) throw new
|
|
1791
|
+
if (s.data.transient && SCOPE_METHODS.has(method)) throw new RelayError3("scope_refused", "a hook connection cannot read or change scopes, groups or grants");
|
|
2166
1792
|
const me = endpointOf(s);
|
|
2167
|
-
if (s.data.transient && !TRANSIENT_METHODS.has(method)) throw new
|
|
1793
|
+
if (s.data.transient && !TRANSIENT_METHODS.has(method)) throw new RelayError3("unauthorized", "a hook connection only posts activity and status");
|
|
2168
1794
|
if (OBSERVER_METHODS.has(method)) return observe(s, method, params);
|
|
2169
1795
|
if (SCOPE_METHODS.has(method)) return scope(s, method, params);
|
|
2170
1796
|
switch (method) {
|
|
@@ -2172,13 +1798,13 @@ async function startBroker(options) {
|
|
|
2172
1798
|
return s.data.transient ? core.whoami(me) : { ...core.whoami(me), ...core.peekState(me) };
|
|
2173
1799
|
case "peek_turn":
|
|
2174
1800
|
if (!s.data.transient || !core.hookOwns(me, s.data.process))
|
|
2175
|
-
throw new
|
|
1801
|
+
throw new RelayError3("unauthorized", "only a hook of the endpoint's own harness process posts digest turns");
|
|
2176
1802
|
return core.peekTurn(me, { role: params.role, text: params.text });
|
|
2177
1803
|
case "send":
|
|
2178
1804
|
case "ask": {
|
|
2179
|
-
for (const field of SERVER_FIELDS) if (field in params) throw new
|
|
1805
|
+
for (const field of SERVER_FIELDS) if (field in params) throw new RelayError3("invalid_params", `${field} is set by the relay, not the sender`);
|
|
2180
1806
|
loadPolicy();
|
|
2181
|
-
if (policyError) throw new
|
|
1807
|
+
if (policyError) throw new RelayError3("policy_refused", policyError);
|
|
2182
1808
|
const attachments = checkAttachments(params.attachments, s);
|
|
2183
1809
|
const send = { ...params, ...attachments ? { attachments } : {} };
|
|
2184
1810
|
if (method === "ask") send.kind = "request";
|
|
@@ -2194,16 +1820,16 @@ async function startBroker(options) {
|
|
|
2194
1820
|
case "hold_release": {
|
|
2195
1821
|
const id = required(reviewer(s, params), "id");
|
|
2196
1822
|
loadPolicy();
|
|
2197
|
-
if (policyError) throw new
|
|
1823
|
+
if (policyError) throw new RelayError3("policy_refused", policyError);
|
|
2198
1824
|
return judge.release(id, { redacted: params.redacted === true, by: core.addressOf(me) }, deliver);
|
|
2199
1825
|
}
|
|
2200
1826
|
case "hold_refuse": {
|
|
2201
1827
|
const reason = required(reviewer(s, params), "reason");
|
|
2202
|
-
if (reason.length >
|
|
1828
|
+
if (reason.length > LIMITS.noteChars) throw new RelayError3("invalid_params", `reason takes at most ${LIMITS.noteChars} characters`);
|
|
2203
1829
|
return judge.refuse(required(params, "id"), stripControl(reason), core.addressOf(me));
|
|
2204
1830
|
}
|
|
2205
1831
|
case "ask_wait": {
|
|
2206
|
-
if (typeof params.id !== "string") throw new
|
|
1832
|
+
if (typeof params.id !== "string") throw new RelayError3("invalid_params", "id is the request's message id");
|
|
2207
1833
|
return awaitReply(s, me, params.id, waitSeconds(params.wait_s));
|
|
2208
1834
|
}
|
|
2209
1835
|
case "inbox": {
|
|
@@ -2213,7 +1839,7 @@ async function startBroker(options) {
|
|
|
2213
1839
|
let page = read();
|
|
2214
1840
|
while (!page.messages.length && !s.data.closed && Date.now() < deadline) {
|
|
2215
1841
|
await waitFor(me, deadline - Date.now(), s);
|
|
2216
|
-
if (closing) throw new
|
|
1842
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
2217
1843
|
page = read();
|
|
2218
1844
|
}
|
|
2219
1845
|
const { messages } = page;
|
|
@@ -2226,6 +1852,37 @@ async function startBroker(options) {
|
|
|
2226
1852
|
bridge?.changed(me);
|
|
2227
1853
|
return view;
|
|
2228
1854
|
}
|
|
1855
|
+
case "title_infer": {
|
|
1856
|
+
const changed = core.inferTitle(me, params, hookOf(s));
|
|
1857
|
+
if (changed) bridge?.changed(me);
|
|
1858
|
+
return { changed };
|
|
1859
|
+
}
|
|
1860
|
+
case "rename": {
|
|
1861
|
+
const renamed = core.rename(actorOf(s), { ...params.endpoint !== void 0 ? { endpoint: params.endpoint } : {}, handle: params.handle });
|
|
1862
|
+
membership(renamed.endpoint, renamed.session, true, renamed.from);
|
|
1863
|
+
return renamed.endpoint;
|
|
1864
|
+
}
|
|
1865
|
+
case "adopt": {
|
|
1866
|
+
const actor = actorOf(s);
|
|
1867
|
+
const adopted = core.adopt(
|
|
1868
|
+
actor,
|
|
1869
|
+
{ endpoint: params.endpoint, ...params.parent !== void 0 ? { parent: params.parent } : {} },
|
|
1870
|
+
{ tty: actor.kind === "endpoint" && s.data.terminal === true }
|
|
1871
|
+
);
|
|
1872
|
+
bridge?.resync();
|
|
1873
|
+
return adopted;
|
|
1874
|
+
}
|
|
1875
|
+
case "unlink": {
|
|
1876
|
+
const unlinked = core.unlink(actorOf(s), params.endpoint !== void 0 ? { endpoint: params.endpoint } : {});
|
|
1877
|
+
bridge?.resync();
|
|
1878
|
+
return unlinked;
|
|
1879
|
+
}
|
|
1880
|
+
case "leave": {
|
|
1881
|
+
const left = core.leaveSession(actorOf(s), params.endpoint !== void 0 ? { endpoint: params.endpoint } : {});
|
|
1882
|
+
for (const c of connections) if (c.data.endpoint === left.endpoint.id && !c.data.transient && !c.data.lineage && c !== s) c.end();
|
|
1883
|
+
membership(left.endpoint, left.session, false);
|
|
1884
|
+
return left.endpoint;
|
|
1885
|
+
}
|
|
2229
1886
|
case "status": {
|
|
2230
1887
|
core.status(me, params.state, params.note);
|
|
2231
1888
|
const view = core.whoami(me);
|
|
@@ -2241,14 +1898,25 @@ async function startBroker(options) {
|
|
|
2241
1898
|
if ((known.get(me) ?? "offline") !== after.presence) presenceChanged(after);
|
|
2242
1899
|
return row;
|
|
2243
1900
|
}
|
|
2244
|
-
case "inject":
|
|
2245
|
-
|
|
1901
|
+
case "inject": {
|
|
1902
|
+
const waitMs = waitSeconds(params.wait_s);
|
|
1903
|
+
const options2 = {
|
|
2246
1904
|
stop: params.stop === true,
|
|
2247
1905
|
count: params.count === true,
|
|
2248
1906
|
holdUnread: params.hold_unread === true,
|
|
2249
1907
|
directives: params.directives === true,
|
|
1908
|
+
wake: waitMs > 0 && params.stop !== true && params.count !== true,
|
|
2250
1909
|
...hookOf(s)
|
|
2251
|
-
}
|
|
1910
|
+
};
|
|
1911
|
+
const deadline = Date.now() + (options2.wake ? waitMs : 0);
|
|
1912
|
+
let result = core.inject(me, options2);
|
|
1913
|
+
while (!result.offered.length && !s.data.closed && Date.now() < deadline) {
|
|
1914
|
+
await waitFor(me, deadline - Date.now(), s);
|
|
1915
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
1916
|
+
result = core.inject(me, options2);
|
|
1917
|
+
}
|
|
1918
|
+
return result;
|
|
1919
|
+
}
|
|
2252
1920
|
case "stop_wait": {
|
|
2253
1921
|
const waitMs = waitSeconds(params.wait_s);
|
|
2254
1922
|
const deadline = Date.now() + waitMs;
|
|
@@ -2258,7 +1926,7 @@ async function startBroker(options) {
|
|
|
2258
1926
|
const idle = (r) => !r.messages.length && !r.more && r.workers.length + r.starting > 0 && r.pushes_left > 0 && !r.prompted;
|
|
2259
1927
|
while (idle(result) && !s.data.closed && Date.now() < deadline) {
|
|
2260
1928
|
await waitFor(me, Math.min(STOP_RECHECK_MS, deadline - Date.now()), s);
|
|
2261
|
-
if (closing) throw new
|
|
1929
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
2262
1930
|
result = core.stopWait(me, peek);
|
|
2263
1931
|
}
|
|
2264
1932
|
if (waitMs > 0) await gather(me, result, peek, s);
|
|
@@ -2267,7 +1935,8 @@ async function startBroker(options) {
|
|
|
2267
1935
|
case "prompted": {
|
|
2268
1936
|
const prompted = core.prompted(me, hookOf(s));
|
|
2269
1937
|
if (prompted) wake(me);
|
|
2270
|
-
|
|
1938
|
+
const tldr = prompted && typeof params.tldr_every === "number" && core.tldrDue(me, params.tldr_every);
|
|
1939
|
+
return { prompted, ...tldr ? { tldr } : {} };
|
|
2271
1940
|
}
|
|
2272
1941
|
case "injected":
|
|
2273
1942
|
return {
|
|
@@ -2284,13 +1953,13 @@ async function startBroker(options) {
|
|
|
2284
1953
|
case "receipts":
|
|
2285
1954
|
return core.receipts(me, params.id);
|
|
2286
1955
|
}
|
|
2287
|
-
throw new
|
|
1956
|
+
throw new RelayError3("method_not_found", `no method ${method}`);
|
|
2288
1957
|
}
|
|
2289
1958
|
const deliver = async (from, send, origin) => {
|
|
2290
1959
|
let result;
|
|
2291
1960
|
if (remoteSend(core, send.to)) {
|
|
2292
1961
|
if (!bridge)
|
|
2293
|
-
throw new
|
|
1962
|
+
throw new RelayError3("not_found", `no endpoint ${[send.to].flat().join(", ")}: this broker is not linked to a hub (${CLI_NAME} cloud enable)`);
|
|
2294
1963
|
result = await bridge.forward(from, send, origin);
|
|
2295
1964
|
} else {
|
|
2296
1965
|
result = core.send(from, send, origin);
|
|
@@ -2310,7 +1979,7 @@ async function startBroker(options) {
|
|
|
2310
1979
|
return result;
|
|
2311
1980
|
};
|
|
2312
1981
|
function reviewer(s, params) {
|
|
2313
|
-
if (!atTerminal(s)) throw new
|
|
1982
|
+
if (!atTerminal(s)) throw new RelayError3("unauthorized", "only your human at a terminal reviews held messages");
|
|
2314
1983
|
return params;
|
|
2315
1984
|
}
|
|
2316
1985
|
function observe(s, method, params) {
|
|
@@ -2318,7 +1987,7 @@ async function startBroker(options) {
|
|
|
2318
1987
|
switch (method) {
|
|
2319
1988
|
case "list":
|
|
2320
1989
|
if (params.contacts === true) {
|
|
2321
|
-
if (!bridge) throw new
|
|
1990
|
+
if (!bridge) throw new RelayError3("unavailable", `this broker is not linked to a hub (${CLI_NAME} cloud enable), so it reaches no contacts`);
|
|
2322
1991
|
return bridge.contacts().then((contacts) => ({ contacts }));
|
|
2323
1992
|
}
|
|
2324
1993
|
return { endpoints: core.list({ ...params, viewer, peekable: viewer === void 0 }) };
|
|
@@ -2359,7 +2028,7 @@ async function startBroker(options) {
|
|
|
2359
2028
|
case "bridge_relink":
|
|
2360
2029
|
return relink();
|
|
2361
2030
|
}
|
|
2362
|
-
throw new
|
|
2031
|
+
throw new RelayError3("method_not_found", `no method ${method}`);
|
|
2363
2032
|
}
|
|
2364
2033
|
function scope(s, method, params) {
|
|
2365
2034
|
const result = scopeCall(s, method, params);
|
|
@@ -2438,7 +2107,7 @@ async function startBroker(options) {
|
|
|
2438
2107
|
return result;
|
|
2439
2108
|
}
|
|
2440
2109
|
}
|
|
2441
|
-
throw new
|
|
2110
|
+
throw new RelayError3("method_not_found", `no method ${method}`);
|
|
2442
2111
|
}
|
|
2443
2112
|
function realOrSelf(path) {
|
|
2444
2113
|
try {
|
|
@@ -2451,7 +2120,7 @@ async function startBroker(options) {
|
|
|
2451
2120
|
const deadline = Date.now() + ms;
|
|
2452
2121
|
const present = sent?.deliveries.filter((d) => !d.offline).map((d) => d.endpoint);
|
|
2453
2122
|
for (; ; ) {
|
|
2454
|
-
if (closing) throw new
|
|
2123
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
2455
2124
|
const reply = core.replyTo(me, request);
|
|
2456
2125
|
if (reply) {
|
|
2457
2126
|
core.ack(me, [reply.seq]);
|
|
@@ -2482,7 +2151,7 @@ async function startBroker(options) {
|
|
|
2482
2151
|
unlinkSync2(socket);
|
|
2483
2152
|
} catch {
|
|
2484
2153
|
}
|
|
2485
|
-
if (socket !==
|
|
2154
|
+
if (socket !== join4(dir, "relay.sock")) {
|
|
2486
2155
|
try {
|
|
2487
2156
|
rmdirSync(dirname2(socket));
|
|
2488
2157
|
} catch {
|
|
@@ -2503,18 +2172,18 @@ function compact(fields) {
|
|
|
2503
2172
|
function optional(params, key) {
|
|
2504
2173
|
const value = params[key];
|
|
2505
2174
|
if (value === void 0 || value === null) return void 0;
|
|
2506
|
-
if (typeof value !== "string") throw new
|
|
2175
|
+
if (typeof value !== "string") throw new RelayError3("invalid_params", `${key} must be a string`);
|
|
2507
2176
|
return value;
|
|
2508
2177
|
}
|
|
2509
2178
|
function required(params, key) {
|
|
2510
2179
|
const value = optional(params, key);
|
|
2511
|
-
if (!value) throw new
|
|
2180
|
+
if (!value) throw new RelayError3("invalid_params", `${key} is required`);
|
|
2512
2181
|
return value;
|
|
2513
2182
|
}
|
|
2514
2183
|
function selectors(params, key) {
|
|
2515
2184
|
const value = params[key];
|
|
2516
2185
|
if (value === void 0 || value === null) return void 0;
|
|
2517
|
-
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) throw new
|
|
2186
|
+
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) throw new RelayError3("invalid_params", `${key} must be a list of selectors`);
|
|
2518
2187
|
return value;
|
|
2519
2188
|
}
|
|
2520
2189
|
export {
|
|
@@ -2527,11 +2196,13 @@ export {
|
|
|
2527
2196
|
localHostName,
|
|
2528
2197
|
policyPath,
|
|
2529
2198
|
privateDir,
|
|
2199
|
+
readSessionMarker,
|
|
2530
2200
|
relayDir,
|
|
2531
2201
|
relaySocketUrl,
|
|
2532
2202
|
shortHostname,
|
|
2533
2203
|
socketPath,
|
|
2534
2204
|
startBroker,
|
|
2535
2205
|
systemHostname,
|
|
2536
|
-
thisHostname
|
|
2206
|
+
thisHostname,
|
|
2207
|
+
writeSessionMarker
|
|
2537
2208
|
};
|