@scp-dev/broker 0.1.3 → 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 +28 -115
- package/dist/index.js +94 -514
- package/package.json +3 -3
- package/src/client.ts +2 -2
- package/src/index.ts +3 -2
- package/src/judge-gate.ts +14 -545
- package/src/server.ts +51 -18
package/dist/index.js
CHANGED
|
@@ -1060,11 +1060,11 @@ import { setTimeout as sleep2 } from "timers/promises";
|
|
|
1060
1060
|
import {
|
|
1061
1061
|
BROKER_CAPABILITIES,
|
|
1062
1062
|
CLI_NAME,
|
|
1063
|
-
LIMITS
|
|
1063
|
+
LIMITS,
|
|
1064
1064
|
parsePolicy,
|
|
1065
1065
|
PREAMBLE,
|
|
1066
1066
|
PROTOCOL as PROTOCOL3,
|
|
1067
|
-
RelayError as
|
|
1067
|
+
RelayError as RelayError3,
|
|
1068
1068
|
SERVER_FIELDS,
|
|
1069
1069
|
stripControl
|
|
1070
1070
|
} from "@scp-dev/core";
|
|
@@ -1072,460 +1072,20 @@ import { FederatedCore } from "@scp-dev/hub";
|
|
|
1072
1072
|
|
|
1073
1073
|
// src/judge-gate.ts
|
|
1074
1074
|
import { accessSync, constants, readFileSync as readFileSync4 } from "fs";
|
|
1075
|
-
import {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
judgeBoundary,
|
|
1082
|
-
LIMITS,
|
|
1083
|
-
mergeVerdicts,
|
|
1084
|
-
newId as newId2,
|
|
1085
|
-
newJudgeStats,
|
|
1086
|
-
one,
|
|
1087
|
-
RelayError as RelayError3,
|
|
1088
|
-
runJudge,
|
|
1089
|
-
sha256Hex,
|
|
1090
|
-
VerdictCache
|
|
1091
|
-
} from "@scp-dev/core";
|
|
1092
|
-
var HELD_PER_SENDER = 100;
|
|
1093
|
-
var iso = (ms) => ms === null ? null : new Date(ms).toISOString();
|
|
1094
|
-
var JudgeGate = class {
|
|
1095
|
-
constructor(o) {
|
|
1096
|
-
this.o = o;
|
|
1097
|
-
o.sql.all(
|
|
1098
|
-
`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,
|
|
1099
|
-
created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, state TEXT NOT NULL, by TEXT, resolved_at INTEGER)`
|
|
1100
|
-
);
|
|
1101
|
-
o.sql.all(
|
|
1102
|
-
`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,
|
|
1103
|
-
PRIMARY KEY (from_ep, client_id))`
|
|
1104
|
-
);
|
|
1105
|
-
o.sql.all(`UPDATE judge_holds SET state = 'held' WHERE state = 'releasing'`);
|
|
1106
|
-
}
|
|
1107
|
-
o;
|
|
1108
|
-
stats = newJudgeStats();
|
|
1109
|
-
policy;
|
|
1110
|
-
version = "";
|
|
1111
|
-
client = null;
|
|
1112
|
-
cache = new VerdictCache();
|
|
1113
|
-
shadows = /* @__PURE__ */ new Set();
|
|
1114
|
-
/** judged sends in the last hour per budget: `ep:<id>`, `tree:<root id>` and `*` for the whole broker */
|
|
1115
|
-
spent = /* @__PURE__ */ new Map();
|
|
1116
|
-
/** the policy's judge section; a model client only when its key is readable, else the prefilter alone */
|
|
1117
|
-
configure(policy) {
|
|
1118
|
-
this.policy = policy;
|
|
1119
|
-
this.version = policy ? sha256Hex(JSON.stringify(policy)) : "";
|
|
1120
|
-
this.client = null;
|
|
1121
|
-
const model = policy?.model;
|
|
1122
|
-
if (!policy || policy.mode === "off" || !model) return;
|
|
1123
|
-
const key = this.keyReader(model);
|
|
1124
|
-
if (!key) {
|
|
1125
|
-
this.o.log(
|
|
1126
|
-
`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`
|
|
1127
|
-
);
|
|
1128
|
-
return;
|
|
1129
|
-
}
|
|
1130
|
-
this.client = (this.o.client ?? ((m, k) => new HttpJudgeClient({ model: m.id, effort: m.effort, key: k })))(model, key);
|
|
1131
|
-
}
|
|
1132
|
-
keyReader(model) {
|
|
1133
|
-
if (model.key_env) {
|
|
1134
|
-
const name = model.key_env;
|
|
1135
|
-
return this.o.env[name] ? async () => this.o.env[name] ?? "" : null;
|
|
1136
|
-
}
|
|
1137
|
-
const file = model.key_file;
|
|
1138
|
-
try {
|
|
1139
|
-
accessSync(file, constants.R_OK);
|
|
1140
|
-
} catch {
|
|
1141
|
-
return null;
|
|
1142
|
-
}
|
|
1143
|
-
return async () => readFileSync4(file, "utf8").trim();
|
|
1144
|
-
}
|
|
1145
|
-
/** resolves once every shadow judgement started so far finished */
|
|
1146
|
-
async settled() {
|
|
1147
|
-
await Promise.all([...this.shadows]);
|
|
1148
|
-
}
|
|
1149
|
-
/** a send or ask through the judge; an unjudged one goes straight to `deliver` */
|
|
1150
|
-
async send(from, params, origin, deliver) {
|
|
1151
|
-
const policy = this.policy;
|
|
1152
|
-
const body = params.body;
|
|
1153
|
-
if (!policy || policy.mode === "off" || typeof body !== "string" || new TextEncoder().encode(body).length > LIMITS.bodyBytes || findSecret2(body))
|
|
1154
|
-
return deliver(from, params, origin);
|
|
1155
|
-
const targets = Array.isArray(params.to) ? params.to : [params.to];
|
|
1156
|
-
if (targets.some((to) => this.o.core.foreignAccount(to) !== null)) {
|
|
1157
|
-
if (policy.mode === "enforce") {
|
|
1158
|
-
this.o.core.auditJudge("judge_skipped", from, null, {
|
|
1159
|
-
side: "local",
|
|
1160
|
-
boundary: "other account",
|
|
1161
|
-
prompt_version: JUDGE_PROMPT_VERSION,
|
|
1162
|
-
categories: [],
|
|
1163
|
-
sha256: sha256Hex(body),
|
|
1164
|
-
verdict: "skipped",
|
|
1165
|
-
reason: "other_account_judged_by_hub",
|
|
1166
|
-
to: targets.map(String)
|
|
1167
|
-
});
|
|
1168
|
-
}
|
|
1169
|
-
return deliver(from, params, origin);
|
|
1170
|
-
}
|
|
1171
|
-
if (policy.mode === "shadow") {
|
|
1172
|
-
const result = await deliver(from, params, origin);
|
|
1173
|
-
if (!result.duplicate) this.shadow(policy, from, params, origin, result.id);
|
|
1174
|
-
return result;
|
|
1175
|
-
}
|
|
1176
|
-
const { groups, tree } = this.boundaries(policy, from, params, origin);
|
|
1177
|
-
if (!groups.size) return deliver(from, params, origin);
|
|
1178
|
-
const ttl = params.ttl_s ?? LIMITS.ttlDefaultS;
|
|
1179
|
-
if (!Number.isInteger(ttl) || ttl < 1 || ttl > LIMITS.ttlMaxS) throw new RelayError3("invalid_params", `ttl_s must be 1 to ${LIMITS.ttlMaxS}`);
|
|
1180
|
-
const seen = params.client_id === void 0 ? void 0 : this.seen(from, params);
|
|
1181
|
-
if (seen) return seen;
|
|
1182
|
-
const waiting = one(this.o.sql, `SELECT COUNT(*) AS n FROM judge_holds WHERE from_ep = ? AND state IN ('held', 'releasing')`, from).n;
|
|
1183
|
-
if (waiting >= HELD_PER_SENDER)
|
|
1184
|
-
throw new RelayError3("rate_limited", `${waiting} of your messages wait for review by your human; send more once they released or refused some`, 3600);
|
|
1185
|
-
this.spend(policy, from, tree);
|
|
1186
|
-
const verdict = await this.judge(policy, from, body, groups);
|
|
1187
|
-
const detail = this.detail(verdict, body, groups);
|
|
1188
|
-
switch (verdict.verdict) {
|
|
1189
|
-
case "refuse":
|
|
1190
|
-
this.o.core.auditJudge("judge_refuse", from, null, detail);
|
|
1191
|
-
throw new RelayError3(
|
|
1192
|
-
"judge_refused",
|
|
1193
|
-
`the relay judge refused this message (local; ${verdict.categories.join(", ") || "no category"}): ${verdict.reason}`,
|
|
1194
|
-
void 0,
|
|
1195
|
-
{
|
|
1196
|
-
side: "local",
|
|
1197
|
-
categories: verdict.categories,
|
|
1198
|
-
reason: verdict.reason
|
|
1199
|
-
}
|
|
1200
|
-
);
|
|
1201
|
-
case "hold":
|
|
1202
|
-
return this.hold(from, params, origin, verdict, groups, detail);
|
|
1203
|
-
case "redact": {
|
|
1204
|
-
const applied = applyRedactions(body, verdict.redactions);
|
|
1205
|
-
const result = await deliver(from, { ...params, body: applied.text }, origin);
|
|
1206
|
-
const redacted = { n: applied.n, kinds: applied.kinds };
|
|
1207
|
-
this.o.core.auditJudge(verdict.source === "error" ? "judge_skipped" : "judge_redact", from, result.id, { ...detail, redacted });
|
|
1208
|
-
return this.remember(from, params, { ...result, redacted });
|
|
1209
|
-
}
|
|
1210
|
-
case "pass": {
|
|
1211
|
-
const result = await deliver(from, params, origin);
|
|
1212
|
-
if (verdict.source === "error") this.o.core.auditJudge("judge_skipped", from, result.id, detail);
|
|
1213
|
-
return this.remember(from, params, result);
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
/** 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 */
|
|
1218
|
-
seen(from, params) {
|
|
1219
|
-
const clientId = params.client_id;
|
|
1220
|
-
const held2 = one(
|
|
1221
|
-
this.o.sql,
|
|
1222
|
-
`SELECT id, params FROM judge_holds WHERE from_ep = ? AND json_extract(params, '$.client_id') = ? AND state IN ('held', 'releasing')`,
|
|
1223
|
-
from,
|
|
1224
|
-
clientId
|
|
1225
|
-
);
|
|
1226
|
-
const sent = held2 ? void 0 : one(
|
|
1227
|
-
this.o.sql,
|
|
1228
|
-
`SELECT fingerprint, result FROM judge_sent WHERE from_ep = ? AND client_id = ? AND created_at > ?`,
|
|
1229
|
-
from,
|
|
1230
|
-
clientId,
|
|
1231
|
-
this.o.now() - LIMITS.idempotencyS * 1e3
|
|
1232
|
-
);
|
|
1233
|
-
const before = held2 ? fingerprint(JSON.parse(held2.params)) : sent?.fingerprint;
|
|
1234
|
-
if (before === void 0) return void 0;
|
|
1235
|
-
if (before !== fingerprint(params)) throw new RelayError3("invalid_params", `client_id ${clientId} was already used for a different message`);
|
|
1236
|
-
return held2 ? heldResult(held2.id, params) : { ...JSON.parse(sent.result), duplicate: true };
|
|
1237
|
-
}
|
|
1238
|
-
remember(from, params, result) {
|
|
1239
|
-
if (params.client_id !== void 0 && !result.duplicate) {
|
|
1240
|
-
this.o.sql.all(
|
|
1241
|
-
`INSERT OR REPLACE INTO judge_sent (from_ep, client_id, fingerprint, result, created_at) VALUES (?, ?, ?, ?, ?)`,
|
|
1242
|
-
from,
|
|
1243
|
-
params.client_id,
|
|
1244
|
-
fingerprint(params),
|
|
1245
|
-
JSON.stringify(result),
|
|
1246
|
-
this.o.now()
|
|
1247
|
-
);
|
|
1248
|
-
}
|
|
1249
|
-
return result;
|
|
1250
|
-
}
|
|
1251
|
-
/**
|
|
1252
|
-
* 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
|
|
1253
|
-
* held sends never reach the send's own limits, and a new handle or a helper process must not bring a fresh budget
|
|
1254
|
-
*/
|
|
1255
|
-
spend(policy, from, tree) {
|
|
1256
|
-
const now = this.o.now();
|
|
1257
|
-
const global = policy.budget ?? DEFAULT_JUDGE_BUDGET;
|
|
1258
|
-
const budgets = [
|
|
1259
|
-
[`ep:${from}`, LIMITS.pairPerMinute, LIMITS.pairPerHour, "from this endpoint"],
|
|
1260
|
-
[`tree:${tree}`, LIMITS.pairPerMinute * 2, LIMITS.pairPerHour * 2, "from this lineage tree"],
|
|
1261
|
-
["*", global.per_minute, global.per_hour, "on this broker"]
|
|
1262
|
-
];
|
|
1263
|
-
const recent = budgets.map(([key]) => (this.spent.get(key) ?? []).filter((at) => at > now - 36e5));
|
|
1264
|
-
budgets.forEach(([key, perMinute, perHour, what], i) => {
|
|
1265
|
-
for (const [limit, windowS] of [
|
|
1266
|
-
[perMinute, 60],
|
|
1267
|
-
[perHour, 3600]
|
|
1268
|
-
]) {
|
|
1269
|
-
const inWindow = recent[i].filter((at) => at > now - windowS * 1e3);
|
|
1270
|
-
if (inWindow.length >= limit) {
|
|
1271
|
-
this.spent.set(key, recent[i]);
|
|
1272
|
-
throw new RelayError3(
|
|
1273
|
-
"rate_limited",
|
|
1274
|
-
`${limit} judged messages ${what} in ${windowS} s`,
|
|
1275
|
-
Math.max(1, Math.ceil((inWindow[0] + windowS * 1e3 - now) / 1e3))
|
|
1276
|
-
);
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
});
|
|
1280
|
-
for (const [i, [key]] of budgets.entries()) this.spent.set(key, [...recent[i], now]);
|
|
1281
|
-
}
|
|
1282
|
-
/** how many budgets are tracked (idle ones are evicted by the sweep) */
|
|
1283
|
-
budgetKeys() {
|
|
1284
|
-
return this.spent.size;
|
|
1285
|
-
}
|
|
1286
|
-
/** the held messages, newest first, without bodies */
|
|
1287
|
-
holds(state) {
|
|
1288
|
-
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`);
|
|
1289
|
-
return rows.map((row) => this.view(row));
|
|
1290
|
-
}
|
|
1291
|
-
/** one hold with its body and the judge's redactions while it waits */
|
|
1292
|
-
show(id) {
|
|
1293
|
-
const row = this.row(id);
|
|
1294
|
-
const params = JSON.parse(row.params);
|
|
1295
|
-
const verdict = JSON.parse(row.verdict);
|
|
1296
|
-
return { ...this.view(row), ...typeof params.body === "string" ? { body: params.body } : {}, redactions: verdict.redactions };
|
|
1297
|
-
}
|
|
1298
|
-
/** delivers a held message as its sender, as stored or redacted: scope, policy and limits are checked again by the send itself */
|
|
1299
|
-
async release(id, options, deliver) {
|
|
1300
|
-
const row = one(
|
|
1301
|
-
this.o.sql,
|
|
1302
|
-
`UPDATE judge_holds SET state = 'releasing' WHERE id = ? AND state = 'held' AND expires_at > ? RETURNING *`,
|
|
1303
|
-
id,
|
|
1304
|
-
this.o.now()
|
|
1305
|
-
);
|
|
1306
|
-
if (!row) throw new RelayError3("not_found", `no held message ${id}`);
|
|
1307
|
-
const params = JSON.parse(row.params);
|
|
1308
|
-
const verdict = JSON.parse(row.verdict);
|
|
1309
|
-
let sent = params;
|
|
1310
|
-
let redacted;
|
|
1311
|
-
if (options.redacted) {
|
|
1312
|
-
const applied = applyRedactions(params.body, verdict.redactions);
|
|
1313
|
-
if (!applied.n) {
|
|
1314
|
-
this.o.sql.all(`UPDATE judge_holds SET state = 'held' WHERE id = ?`, id);
|
|
1315
|
-
throw new RelayError3("invalid_params", `hold ${id} has no redactions: release it as it is or refuse it`);
|
|
1316
|
-
}
|
|
1317
|
-
sent = { ...params, body: applied.text };
|
|
1318
|
-
redacted = { n: applied.n, kinds: applied.kinds };
|
|
1319
|
-
}
|
|
1320
|
-
let result;
|
|
1321
|
-
try {
|
|
1322
|
-
result = await deliver(row.from_ep, sent, JSON.parse(row.origin));
|
|
1323
|
-
} catch (error) {
|
|
1324
|
-
this.o.sql.all(`UPDATE judge_holds SET state = 'held' WHERE id = ?`, id);
|
|
1325
|
-
throw error;
|
|
1326
|
-
}
|
|
1327
|
-
this.remember(row.from_ep, params, { ...result, ...redacted ? { redacted } : {} });
|
|
1328
|
-
this.resolve(row, "released", options.by);
|
|
1329
|
-
this.o.core.auditJudge("judge_override", row.from_ep, id, {
|
|
1330
|
-
...this.storedDetail(row, verdict),
|
|
1331
|
-
by: options.by,
|
|
1332
|
-
action: redacted ? "release_redacted" : "release",
|
|
1333
|
-
...redacted ? { redacted } : {}
|
|
1334
|
-
});
|
|
1335
|
-
this.tell(
|
|
1336
|
-
row.from_ep,
|
|
1337
|
-
`relay: your message held for review as ${id} was released by your human${redacted ? " with redactions" : ""} and delivered as ${result.id}`
|
|
1338
|
-
);
|
|
1339
|
-
return { ...result, ...redacted ? { redacted } : {} };
|
|
1340
|
-
}
|
|
1341
|
-
/** refuses a held message: it is never delivered, and its sender hears the human's reason */
|
|
1342
|
-
refuse(id, reason, by) {
|
|
1343
|
-
this.expire(this.o.now());
|
|
1344
|
-
const row = one(this.o.sql, `SELECT * FROM judge_holds WHERE id = ? AND state = 'held'`, id);
|
|
1345
|
-
if (!row) throw new RelayError3("not_found", `no held message ${id}`);
|
|
1346
|
-
const verdict = JSON.parse(row.verdict);
|
|
1347
|
-
this.resolve(row, "refused", by);
|
|
1348
|
-
this.o.core.auditJudge("judge_override", row.from_ep, id, { ...this.storedDetail(row, verdict), by, action: "refuse", reason });
|
|
1349
|
-
this.tell(row.from_ep, `relay: your message held for review as ${id} was refused by your human: ${reason}`);
|
|
1350
|
-
return this.view(this.row(id));
|
|
1351
|
-
}
|
|
1352
|
-
/** expires holds past their message TTL (body purged, sender told) and drops resolved ones past the audit window */
|
|
1353
|
-
sweep() {
|
|
1354
|
-
const now = this.o.now();
|
|
1355
|
-
this.expire(now);
|
|
1356
|
-
this.o.sql.all(`DELETE FROM judge_holds WHERE state IN ('released', 'refused', 'expired') AND resolved_at <= ?`, now - LIMITS.auditKeepS * 1e3);
|
|
1357
|
-
this.o.sql.all(`DELETE FROM judge_sent WHERE created_at <= ?`, now - LIMITS.idempotencyS * 1e3);
|
|
1358
|
-
for (const [key, times] of this.spent) if (!times.length || times[times.length - 1] <= now - 36e5) this.spent.delete(key);
|
|
1359
|
-
}
|
|
1360
|
-
expire(now) {
|
|
1361
|
-
for (const row of this.o.sql.all(`SELECT * FROM judge_holds WHERE state = 'held' AND expires_at <= ?`, now)) {
|
|
1362
|
-
const verdict = JSON.parse(row.verdict);
|
|
1363
|
-
this.resolve(row, "expired", null);
|
|
1364
|
-
this.o.core.auditJudge("judge_expire", row.from_ep, row.id, this.storedDetail(row, verdict));
|
|
1365
|
-
this.tell(row.from_ep, `relay: your message held for review as ${row.id} expired without review and was not delivered`);
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
/** the recipients by the boundary their message crosses; throws as send does for a target the sender cannot reach */
|
|
1369
|
-
boundaries(policy, from, params, origin) {
|
|
1370
|
-
const { sender, recipients, tree } = this.o.core.judgeSubjects(from, params, origin);
|
|
1371
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1372
|
-
for (const recipient of recipients) {
|
|
1373
|
-
const label = judgeBoundary(policy, sender, recipient);
|
|
1374
|
-
if (label !== null) groups.set(label, [...groups.get(label) ?? [], recipient.address]);
|
|
1375
|
-
}
|
|
1376
|
-
return { groups, tree };
|
|
1377
|
-
}
|
|
1378
|
-
async judge(policy, from, body, groups) {
|
|
1379
|
-
const verdicts = await Promise.all(
|
|
1380
|
-
[...groups].map(
|
|
1381
|
-
([boundary, to]) => runJudge({
|
|
1382
|
-
body,
|
|
1383
|
-
side: "local",
|
|
1384
|
-
boundary,
|
|
1385
|
-
configVersion: this.version,
|
|
1386
|
-
client: this.client,
|
|
1387
|
-
cache: this.cache,
|
|
1388
|
-
timeoutMs: policy.timeout_s * 1e3,
|
|
1389
|
-
failMode: policy.fail,
|
|
1390
|
-
...this.client && policy.model ? { model: policy.model.id } : {},
|
|
1391
|
-
from: this.o.core.addressOf(from),
|
|
1392
|
-
to,
|
|
1393
|
-
now: this.o.now,
|
|
1394
|
-
stats: this.stats
|
|
1395
|
-
})
|
|
1396
|
-
)
|
|
1397
|
-
);
|
|
1398
|
-
return mergeVerdicts(verdicts);
|
|
1399
|
-
}
|
|
1400
|
-
shadow(policy, from, params, origin, ref) {
|
|
1401
|
-
const task = (async () => {
|
|
1402
|
-
try {
|
|
1403
|
-
const { groups, tree } = this.boundaries(policy, from, params, origin);
|
|
1404
|
-
if (!groups.size) return;
|
|
1405
|
-
this.spend(policy, from, tree);
|
|
1406
|
-
const body = params.body;
|
|
1407
|
-
const verdict = await this.judge(policy, from, body, groups);
|
|
1408
|
-
this.o.core.auditJudge("judge_shadow", from, ref, this.detail(verdict, body, groups));
|
|
1409
|
-
} catch (error) {
|
|
1410
|
-
this.o.log(`relay broker: a shadow judgement failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1411
|
-
}
|
|
1412
|
-
})();
|
|
1413
|
-
this.shadows.add(task);
|
|
1414
|
-
void task.finally(() => this.shadows.delete(task));
|
|
1415
|
-
}
|
|
1416
|
-
hold(from, params, origin, verdict, groups, detail) {
|
|
1417
|
-
const now = this.o.now();
|
|
1418
|
-
const id = newId2("hold", now);
|
|
1419
|
-
const stored = { ...params, client_id: params.client_id ?? id };
|
|
1420
|
-
const to = [...groups.values()].flat();
|
|
1421
|
-
const record = { ...verdict, boundary: [...groups.keys()].join(" | "), to };
|
|
1422
|
-
this.o.sql.all(
|
|
1423
|
-
`INSERT INTO judge_holds (id, from_ep, params, origin, verdict, created_at, expires_at, state) VALUES (?, ?, ?, ?, ?, ?, ?, 'held')`,
|
|
1424
|
-
id,
|
|
1425
|
-
from,
|
|
1426
|
-
JSON.stringify(stored),
|
|
1427
|
-
JSON.stringify(origin),
|
|
1428
|
-
JSON.stringify(record),
|
|
1429
|
-
now,
|
|
1430
|
-
now + (params.ttl_s ?? LIMITS.ttlDefaultS) * 1e3
|
|
1431
|
-
);
|
|
1432
|
-
this.o.core.auditJudge("judge_hold", from, id, detail);
|
|
1433
|
-
return heldResult(id, params);
|
|
1434
|
-
}
|
|
1435
|
-
detail(verdict, body, groups) {
|
|
1436
|
-
return {
|
|
1437
|
-
side: "local",
|
|
1438
|
-
boundary: [...groups.keys()].join(" | "),
|
|
1439
|
-
prompt_version: verdict.prompt_version,
|
|
1440
|
-
categories: verdict.categories,
|
|
1441
|
-
sha256: sha256Hex(body),
|
|
1442
|
-
verdict: verdict.verdict,
|
|
1443
|
-
source: verdict.source,
|
|
1444
|
-
reason: verdict.reason,
|
|
1445
|
-
to: [...groups.values()].flat(),
|
|
1446
|
-
...verdict.model ? { model: verdict.model } : {},
|
|
1447
|
-
...verdict.ms !== void 0 ? { ms: verdict.ms } : {}
|
|
1448
|
-
};
|
|
1449
|
-
}
|
|
1450
|
-
storedDetail(row, verdict) {
|
|
1451
|
-
const params = JSON.parse(row.params);
|
|
1452
|
-
return {
|
|
1453
|
-
side: "local",
|
|
1454
|
-
boundary: verdict.boundary,
|
|
1455
|
-
prompt_version: verdict.prompt_version,
|
|
1456
|
-
categories: verdict.categories,
|
|
1457
|
-
sha256: params.sha256 ?? sha256Hex(params.body ?? ""),
|
|
1458
|
-
verdict: verdict.verdict,
|
|
1459
|
-
to: verdict.to,
|
|
1460
|
-
...verdict.model ? { model: verdict.model } : {}
|
|
1461
|
-
};
|
|
1462
|
-
}
|
|
1463
|
-
/** marks a hold resolved and purges its body and the judge's quotes of it */
|
|
1464
|
-
resolve(row, state, by) {
|
|
1465
|
-
const { body, ...params } = JSON.parse(row.params);
|
|
1466
|
-
const verdict = JSON.parse(row.verdict);
|
|
1467
|
-
const purged = { ...params, bytes: new TextEncoder().encode(body).length, sha256: sha256Hex(body) };
|
|
1468
|
-
const kinds = { ...verdict, redactions: verdict.redactions.map((r) => ({ kind: r.kind })) };
|
|
1469
|
-
this.o.sql.all(
|
|
1470
|
-
`UPDATE judge_holds SET state = ?, by = ?, resolved_at = ?, params = ?, verdict = ? WHERE id = ?`,
|
|
1471
|
-
state,
|
|
1472
|
-
by,
|
|
1473
|
-
this.o.now(),
|
|
1474
|
-
JSON.stringify(purged),
|
|
1475
|
-
JSON.stringify(kinds),
|
|
1476
|
-
row.id
|
|
1477
|
-
);
|
|
1478
|
-
}
|
|
1479
|
-
tell(endpoint, text) {
|
|
1480
|
-
try {
|
|
1481
|
-
this.o.core.judgeNotice(endpoint, text);
|
|
1482
|
-
this.o.wake(endpoint);
|
|
1483
|
-
} catch (error) {
|
|
1484
|
-
if (!(error instanceof RelayError3)) throw error;
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
row(id) {
|
|
1488
|
-
const row = one(this.o.sql, `SELECT * FROM judge_holds WHERE id = ?`, id);
|
|
1489
|
-
if (!row) throw new RelayError3("not_found", `no held message ${id}`);
|
|
1490
|
-
return row;
|
|
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;
|
|
1491
1081
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
from: this.o.core.addressOf(row.from_ep),
|
|
1498
|
-
to: verdict.to,
|
|
1499
|
-
state: row.state,
|
|
1500
|
-
created_at: iso(row.created_at),
|
|
1501
|
-
expires_at: iso(row.expires_at),
|
|
1502
|
-
categories: verdict.categories,
|
|
1503
|
-
reason: verdict.reason,
|
|
1504
|
-
boundary: verdict.boundary,
|
|
1505
|
-
source: verdict.source,
|
|
1506
|
-
bytes: params.bytes ?? new TextEncoder().encode(params.body ?? "").length,
|
|
1507
|
-
by: row.by,
|
|
1508
|
-
resolved_at: iso(row.resolved_at)
|
|
1509
|
-
};
|
|
1082
|
+
return async () => readFileSync4(file, "utf8").trim();
|
|
1083
|
+
}
|
|
1084
|
+
var JudgeGate = class extends HubJudgeGate {
|
|
1085
|
+
constructor(options) {
|
|
1086
|
+
super({ keyFile: readKeyFile, ...options });
|
|
1510
1087
|
}
|
|
1511
1088
|
};
|
|
1512
|
-
var fingerprint = (p) => sha256Hex(
|
|
1513
|
-
JSON.stringify([
|
|
1514
|
-
[p.to].flat().map(String).sort(),
|
|
1515
|
-
p.body,
|
|
1516
|
-
p.kind ?? "message",
|
|
1517
|
-
p.thread ?? null,
|
|
1518
|
-
p.reply_to ?? null,
|
|
1519
|
-
p.data_class ?? "public",
|
|
1520
|
-
(p.attachments ?? []).map((a) => a?.path).sort(),
|
|
1521
|
-
p.mime ?? null,
|
|
1522
|
-
p.ttl_s ?? LIMITS.ttlDefaultS,
|
|
1523
|
-
p.require_reply ?? null
|
|
1524
|
-
])
|
|
1525
|
-
);
|
|
1526
|
-
function heldResult(id, params) {
|
|
1527
|
-
return { id, thread: params.thread ?? "", hop: 0, deliveries: [], held: { hold_id: id, side: "local" } };
|
|
1528
|
-
}
|
|
1529
1089
|
|
|
1530
1090
|
// src/sessions.ts
|
|
1531
1091
|
import { createHash as createHash2, randomUUID } from "crypto";
|
|
@@ -1773,21 +1333,17 @@ async function startBroker(options) {
|
|
|
1773
1333
|
}
|
|
1774
1334
|
for (const id of known.keys()) if (!live.has(id)) known.delete(id);
|
|
1775
1335
|
}, options.presenceEveryMs ?? PRESENCE_MS);
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
log
|
|
1788
|
-
}
|
|
1789
|
-
});
|
|
1790
|
-
}
|
|
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) });
|
|
1791
1347
|
async function relink() {
|
|
1792
1348
|
const config = options.bridgeConfig ? await options.bridgeConfig() : options.bridge;
|
|
1793
1349
|
if (!config || bridge && config.url === options.bridge?.url && config.machine === options.bridge?.machine) {
|
|
@@ -1943,7 +1499,7 @@ async function startBroker(options) {
|
|
|
1943
1499
|
endpoint: core.whoami(endpoint),
|
|
1944
1500
|
capabilities: BROKER_CAPABILITIES,
|
|
1945
1501
|
server_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1946
|
-
limits:
|
|
1502
|
+
limits: LIMITS,
|
|
1947
1503
|
preamble: PREAMBLE,
|
|
1948
1504
|
broker: brokerInfo()
|
|
1949
1505
|
};
|
|
@@ -1973,16 +1529,16 @@ async function startBroker(options) {
|
|
|
1973
1529
|
}
|
|
1974
1530
|
async function gather(me, result, peek, s) {
|
|
1975
1531
|
const gathering = (r) => workerResults(r) > 0 && !r.more && !r.prompted && othersRunning(r) > 0;
|
|
1976
|
-
const cap = Date.now() +
|
|
1977
|
-
let quiet = Date.now() +
|
|
1532
|
+
const cap = Date.now() + LIMITS.pushGraceS * 1e3;
|
|
1533
|
+
let quiet = Date.now() + LIMITS.pushQuietS * 1e3;
|
|
1978
1534
|
let results = workerResults(result);
|
|
1979
1535
|
while (gathering(result) && !s.data.closed && Date.now() < Math.min(quiet, cap)) {
|
|
1980
1536
|
await waitFor(me, Math.min(quiet, cap) - Date.now(), s);
|
|
1981
|
-
if (closing) throw new
|
|
1537
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
1982
1538
|
result = core.stopWait(me, peek);
|
|
1983
1539
|
if (workerResults(result) > results) {
|
|
1984
1540
|
results = workerResults(result);
|
|
1985
|
-
quiet = Date.now() +
|
|
1541
|
+
quiet = Date.now() + LIMITS.pushQuietS * 1e3;
|
|
1986
1542
|
}
|
|
1987
1543
|
}
|
|
1988
1544
|
}
|
|
@@ -2076,7 +1632,7 @@ async function startBroker(options) {
|
|
|
2076
1632
|
if (hasId) write(s, { jsonrpc: "2.0", id, result });
|
|
2077
1633
|
} catch (error) {
|
|
2078
1634
|
if (!hasId || closing) return;
|
|
2079
|
-
if (error instanceof
|
|
1635
|
+
if (error instanceof RelayError3) {
|
|
2080
1636
|
const code = error.code === "method_not_found" ? -32601 : error.code === "invalid_params" ? -32602 : -32e3;
|
|
2081
1637
|
write(s, {
|
|
2082
1638
|
jsonrpc: "2.0",
|
|
@@ -2097,64 +1653,70 @@ async function startBroker(options) {
|
|
|
2097
1653
|
}
|
|
2098
1654
|
}
|
|
2099
1655
|
function endpointOf(s) {
|
|
2100
|
-
if (closing) throw new
|
|
1656
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
2101
1657
|
if (!s.data.endpoint)
|
|
2102
|
-
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");
|
|
2103
1659
|
core.touch(s.data.endpoint);
|
|
2104
1660
|
return s.data.endpoint;
|
|
2105
1661
|
}
|
|
2106
1662
|
function waitSeconds(value) {
|
|
2107
1663
|
if (value === void 0) return 0;
|
|
2108
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new
|
|
2109
|
-
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;
|
|
2110
1666
|
}
|
|
2111
1667
|
function checkAttachments(attachments, s) {
|
|
2112
1668
|
if (attachments === void 0) return void 0;
|
|
2113
|
-
if (!Array.isArray(attachments)) throw new
|
|
1669
|
+
if (!Array.isArray(attachments)) throw new RelayError3("invalid_params", "attachments must be a list");
|
|
2114
1670
|
const tooWide = /* @__PURE__ */ new Set(["/", realOrSelf(homedir2()), realOrSelf(accountHome())]);
|
|
2115
1671
|
const roots = [s.data.cwd, ...options.roots ?? []].filter((r) => typeof r === "string" && isAbsolute(r)).map(realOrSelf).filter((r) => !tooWide.has(r));
|
|
2116
1672
|
return attachments.map((attachment) => {
|
|
2117
1673
|
if (attachment?.kind !== "path" || typeof attachment.path !== "string" || !isAbsolute(attachment.path)) {
|
|
2118
|
-
throw new
|
|
1674
|
+
throw new RelayError3("invalid_params", "an attachment is {kind: path, path: an absolute path}");
|
|
2119
1675
|
}
|
|
2120
1676
|
let real;
|
|
2121
1677
|
try {
|
|
2122
1678
|
real = realpathSync(attachment.path);
|
|
2123
1679
|
} catch {
|
|
2124
|
-
throw new
|
|
1680
|
+
throw new RelayError3("not_found", `no file at ${attachment.path}`);
|
|
2125
1681
|
}
|
|
2126
1682
|
if (!roots.some((root) => real === root || real.startsWith(root.endsWith(sep) ? root : root + sep))) {
|
|
2127
|
-
throw new
|
|
1683
|
+
throw new RelayError3("invalid_params", `${attachment.path} is outside the sender's working directory and the relay's allowed roots`);
|
|
2128
1684
|
}
|
|
2129
1685
|
return { kind: "path", path: real };
|
|
2130
1686
|
});
|
|
2131
1687
|
}
|
|
2132
1688
|
async function dispatch(s, method, params) {
|
|
2133
1689
|
if (method === "hello") {
|
|
2134
|
-
if (s.data.endpoint || s.data.observer) throw new
|
|
2135
|
-
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}`);
|
|
2136
1692
|
if (params.transient === true) {
|
|
2137
1693
|
if (params.human === true || params.observe === true || params.resume !== void 0) {
|
|
2138
|
-
throw new
|
|
1694
|
+
throw new RelayError3("invalid_params", "a hook connection claims an agent endpoint: transient does not go with human, observe or resume");
|
|
2139
1695
|
}
|
|
2140
1696
|
const join5 = params.join;
|
|
2141
1697
|
if (join5 !== void 0 && (typeof join5 !== "object" || join5 === null || Array.isArray(join5)))
|
|
2142
|
-
throw new
|
|
1698
|
+
throw new RelayError3("invalid_params", "join is {handle?, under?}");
|
|
1699
|
+
const under = join5?.under;
|
|
2143
1700
|
const before2 = new Map(core.list({ all: true }).map((view) => [view.id, view]));
|
|
2144
|
-
const { endpoint: endpoint2, subagent, created } = join5 === void 0 ? core.claim(params.claim ?? {}, { transient: true }) : {
|
|
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
|
+
};
|
|
2145
1706
|
s.data.endpoint = endpoint2.id;
|
|
2146
1707
|
s.data.transient = true;
|
|
2147
1708
|
const key = params.claim?.process;
|
|
2148
1709
|
if (typeof key === "string") s.data.process = key;
|
|
2149
1710
|
const was2 = before2.get(endpoint2.id)?.address;
|
|
2150
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();
|
|
2151
1713
|
else if ((before2.get(endpoint2.id)?.presence ?? "offline") !== endpoint2.presence) presenceChanged(endpoint2);
|
|
2152
1714
|
return {
|
|
2153
1715
|
endpoint: endpoint2,
|
|
2154
1716
|
...subagent ? { subagent, ...created ? { created } : {} } : {},
|
|
2155
1717
|
capabilities: BROKER_CAPABILITIES,
|
|
2156
1718
|
server_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2157
|
-
limits:
|
|
1719
|
+
limits: LIMITS,
|
|
2158
1720
|
preamble: PREAMBLE,
|
|
2159
1721
|
broker: brokerInfo()
|
|
2160
1722
|
};
|
|
@@ -2162,7 +1724,7 @@ async function startBroker(options) {
|
|
|
2162
1724
|
if (params.observe === true) {
|
|
2163
1725
|
const self = params.self === void 0 ? null : core.placeLineage(params.self);
|
|
2164
1726
|
if (params.grant !== void 0) {
|
|
2165
|
-
if (!core.grantBinds(params.grant, self)) throw new
|
|
1727
|
+
if (!core.grantBinds(params.grant, self)) throw new RelayError3("unauthorized", GRANT_UNBOUND);
|
|
2166
1728
|
return { ...bindLineage(s, self, params, true), grant_bound: true };
|
|
2167
1729
|
}
|
|
2168
1730
|
if (self) return bindLineage(s, self, params, true);
|
|
@@ -2172,7 +1734,7 @@ async function startBroker(options) {
|
|
|
2172
1734
|
endpoint: null,
|
|
2173
1735
|
capabilities: BROKER_CAPABILITIES,
|
|
2174
1736
|
server_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2175
|
-
limits:
|
|
1737
|
+
limits: LIMITS,
|
|
2176
1738
|
preamble: PREAMBLE,
|
|
2177
1739
|
broker: brokerInfo()
|
|
2178
1740
|
};
|
|
@@ -2180,7 +1742,10 @@ async function startBroker(options) {
|
|
|
2180
1742
|
const claim = params.claim;
|
|
2181
1743
|
if (params.human === true && params.resume === void 0 && params.lineage !== void 0) {
|
|
2182
1744
|
const placed = core.placeLineage(params.lineage);
|
|
2183
|
-
if (placed)
|
|
1745
|
+
if (placed) {
|
|
1746
|
+
s.data.terminal = params.tty === true;
|
|
1747
|
+
return bindLineage(s, placed, params);
|
|
1748
|
+
}
|
|
2184
1749
|
} else if (params.human !== true && params.resume === void 0 && claim?.process === void 0 && typeof claim?.handle === "string") {
|
|
2185
1750
|
const placed = claim.lineage === void 0 ? null : core.placeLineage(claim.lineage, claim.handle);
|
|
2186
1751
|
if (placed && (claim.grant === void 0 || core.grantBinds(claim.grant, placed))) {
|
|
@@ -2215,17 +1780,17 @@ async function startBroker(options) {
|
|
|
2215
1780
|
...token ? { token } : {},
|
|
2216
1781
|
capabilities: BROKER_CAPABILITIES,
|
|
2217
1782
|
server_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2218
|
-
limits:
|
|
1783
|
+
limits: LIMITS,
|
|
2219
1784
|
preamble: PREAMBLE,
|
|
2220
1785
|
broker: brokerInfo()
|
|
2221
1786
|
};
|
|
2222
1787
|
}
|
|
2223
|
-
if (closing) throw new
|
|
1788
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
2224
1789
|
if (s.data.observer && OBSERVER_METHODS.has(method)) return observe(s, method, params);
|
|
2225
1790
|
if (s.data.observer && SCOPE_METHODS.has(method) && (s.data.lineage || method === "why")) return scope(s, method, params);
|
|
2226
|
-
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");
|
|
2227
1792
|
const me = endpointOf(s);
|
|
2228
|
-
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");
|
|
2229
1794
|
if (OBSERVER_METHODS.has(method)) return observe(s, method, params);
|
|
2230
1795
|
if (SCOPE_METHODS.has(method)) return scope(s, method, params);
|
|
2231
1796
|
switch (method) {
|
|
@@ -2233,13 +1798,13 @@ async function startBroker(options) {
|
|
|
2233
1798
|
return s.data.transient ? core.whoami(me) : { ...core.whoami(me), ...core.peekState(me) };
|
|
2234
1799
|
case "peek_turn":
|
|
2235
1800
|
if (!s.data.transient || !core.hookOwns(me, s.data.process))
|
|
2236
|
-
throw new
|
|
1801
|
+
throw new RelayError3("unauthorized", "only a hook of the endpoint's own harness process posts digest turns");
|
|
2237
1802
|
return core.peekTurn(me, { role: params.role, text: params.text });
|
|
2238
1803
|
case "send":
|
|
2239
1804
|
case "ask": {
|
|
2240
|
-
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`);
|
|
2241
1806
|
loadPolicy();
|
|
2242
|
-
if (policyError) throw new
|
|
1807
|
+
if (policyError) throw new RelayError3("policy_refused", policyError);
|
|
2243
1808
|
const attachments = checkAttachments(params.attachments, s);
|
|
2244
1809
|
const send = { ...params, ...attachments ? { attachments } : {} };
|
|
2245
1810
|
if (method === "ask") send.kind = "request";
|
|
@@ -2255,16 +1820,16 @@ async function startBroker(options) {
|
|
|
2255
1820
|
case "hold_release": {
|
|
2256
1821
|
const id = required(reviewer(s, params), "id");
|
|
2257
1822
|
loadPolicy();
|
|
2258
|
-
if (policyError) throw new
|
|
1823
|
+
if (policyError) throw new RelayError3("policy_refused", policyError);
|
|
2259
1824
|
return judge.release(id, { redacted: params.redacted === true, by: core.addressOf(me) }, deliver);
|
|
2260
1825
|
}
|
|
2261
1826
|
case "hold_refuse": {
|
|
2262
1827
|
const reason = required(reviewer(s, params), "reason");
|
|
2263
|
-
if (reason.length >
|
|
1828
|
+
if (reason.length > LIMITS.noteChars) throw new RelayError3("invalid_params", `reason takes at most ${LIMITS.noteChars} characters`);
|
|
2264
1829
|
return judge.refuse(required(params, "id"), stripControl(reason), core.addressOf(me));
|
|
2265
1830
|
}
|
|
2266
1831
|
case "ask_wait": {
|
|
2267
|
-
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");
|
|
2268
1833
|
return awaitReply(s, me, params.id, waitSeconds(params.wait_s));
|
|
2269
1834
|
}
|
|
2270
1835
|
case "inbox": {
|
|
@@ -2274,7 +1839,7 @@ async function startBroker(options) {
|
|
|
2274
1839
|
let page = read();
|
|
2275
1840
|
while (!page.messages.length && !s.data.closed && Date.now() < deadline) {
|
|
2276
1841
|
await waitFor(me, deadline - Date.now(), s);
|
|
2277
|
-
if (closing) throw new
|
|
1842
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
2278
1843
|
page = read();
|
|
2279
1844
|
}
|
|
2280
1845
|
const { messages } = page;
|
|
@@ -2297,6 +1862,21 @@ async function startBroker(options) {
|
|
|
2297
1862
|
membership(renamed.endpoint, renamed.session, true, renamed.from);
|
|
2298
1863
|
return renamed.endpoint;
|
|
2299
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
|
+
}
|
|
2300
1880
|
case "leave": {
|
|
2301
1881
|
const left = core.leaveSession(actorOf(s), params.endpoint !== void 0 ? { endpoint: params.endpoint } : {});
|
|
2302
1882
|
for (const c of connections) if (c.data.endpoint === left.endpoint.id && !c.data.transient && !c.data.lineage && c !== s) c.end();
|
|
@@ -2332,7 +1912,7 @@ async function startBroker(options) {
|
|
|
2332
1912
|
let result = core.inject(me, options2);
|
|
2333
1913
|
while (!result.offered.length && !s.data.closed && Date.now() < deadline) {
|
|
2334
1914
|
await waitFor(me, deadline - Date.now(), s);
|
|
2335
|
-
if (closing) throw new
|
|
1915
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
2336
1916
|
result = core.inject(me, options2);
|
|
2337
1917
|
}
|
|
2338
1918
|
return result;
|
|
@@ -2346,7 +1926,7 @@ async function startBroker(options) {
|
|
|
2346
1926
|
const idle = (r) => !r.messages.length && !r.more && r.workers.length + r.starting > 0 && r.pushes_left > 0 && !r.prompted;
|
|
2347
1927
|
while (idle(result) && !s.data.closed && Date.now() < deadline) {
|
|
2348
1928
|
await waitFor(me, Math.min(STOP_RECHECK_MS, deadline - Date.now()), s);
|
|
2349
|
-
if (closing) throw new
|
|
1929
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
2350
1930
|
result = core.stopWait(me, peek);
|
|
2351
1931
|
}
|
|
2352
1932
|
if (waitMs > 0) await gather(me, result, peek, s);
|
|
@@ -2373,13 +1953,13 @@ async function startBroker(options) {
|
|
|
2373
1953
|
case "receipts":
|
|
2374
1954
|
return core.receipts(me, params.id);
|
|
2375
1955
|
}
|
|
2376
|
-
throw new
|
|
1956
|
+
throw new RelayError3("method_not_found", `no method ${method}`);
|
|
2377
1957
|
}
|
|
2378
1958
|
const deliver = async (from, send, origin) => {
|
|
2379
1959
|
let result;
|
|
2380
1960
|
if (remoteSend(core, send.to)) {
|
|
2381
1961
|
if (!bridge)
|
|
2382
|
-
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)`);
|
|
2383
1963
|
result = await bridge.forward(from, send, origin);
|
|
2384
1964
|
} else {
|
|
2385
1965
|
result = core.send(from, send, origin);
|
|
@@ -2399,7 +1979,7 @@ async function startBroker(options) {
|
|
|
2399
1979
|
return result;
|
|
2400
1980
|
};
|
|
2401
1981
|
function reviewer(s, params) {
|
|
2402
|
-
if (!atTerminal(s)) throw new
|
|
1982
|
+
if (!atTerminal(s)) throw new RelayError3("unauthorized", "only your human at a terminal reviews held messages");
|
|
2403
1983
|
return params;
|
|
2404
1984
|
}
|
|
2405
1985
|
function observe(s, method, params) {
|
|
@@ -2407,7 +1987,7 @@ async function startBroker(options) {
|
|
|
2407
1987
|
switch (method) {
|
|
2408
1988
|
case "list":
|
|
2409
1989
|
if (params.contacts === true) {
|
|
2410
|
-
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`);
|
|
2411
1991
|
return bridge.contacts().then((contacts) => ({ contacts }));
|
|
2412
1992
|
}
|
|
2413
1993
|
return { endpoints: core.list({ ...params, viewer, peekable: viewer === void 0 }) };
|
|
@@ -2448,7 +2028,7 @@ async function startBroker(options) {
|
|
|
2448
2028
|
case "bridge_relink":
|
|
2449
2029
|
return relink();
|
|
2450
2030
|
}
|
|
2451
|
-
throw new
|
|
2031
|
+
throw new RelayError3("method_not_found", `no method ${method}`);
|
|
2452
2032
|
}
|
|
2453
2033
|
function scope(s, method, params) {
|
|
2454
2034
|
const result = scopeCall(s, method, params);
|
|
@@ -2527,7 +2107,7 @@ async function startBroker(options) {
|
|
|
2527
2107
|
return result;
|
|
2528
2108
|
}
|
|
2529
2109
|
}
|
|
2530
|
-
throw new
|
|
2110
|
+
throw new RelayError3("method_not_found", `no method ${method}`);
|
|
2531
2111
|
}
|
|
2532
2112
|
function realOrSelf(path) {
|
|
2533
2113
|
try {
|
|
@@ -2540,7 +2120,7 @@ async function startBroker(options) {
|
|
|
2540
2120
|
const deadline = Date.now() + ms;
|
|
2541
2121
|
const present = sent?.deliveries.filter((d) => !d.offline).map((d) => d.endpoint);
|
|
2542
2122
|
for (; ; ) {
|
|
2543
|
-
if (closing) throw new
|
|
2123
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
2544
2124
|
const reply = core.replyTo(me, request);
|
|
2545
2125
|
if (reply) {
|
|
2546
2126
|
core.ack(me, [reply.seq]);
|
|
@@ -2592,18 +2172,18 @@ function compact(fields) {
|
|
|
2592
2172
|
function optional(params, key) {
|
|
2593
2173
|
const value = params[key];
|
|
2594
2174
|
if (value === void 0 || value === null) return void 0;
|
|
2595
|
-
if (typeof value !== "string") throw new
|
|
2175
|
+
if (typeof value !== "string") throw new RelayError3("invalid_params", `${key} must be a string`);
|
|
2596
2176
|
return value;
|
|
2597
2177
|
}
|
|
2598
2178
|
function required(params, key) {
|
|
2599
2179
|
const value = optional(params, key);
|
|
2600
|
-
if (!value) throw new
|
|
2180
|
+
if (!value) throw new RelayError3("invalid_params", `${key} is required`);
|
|
2601
2181
|
return value;
|
|
2602
2182
|
}
|
|
2603
2183
|
function selectors(params, key) {
|
|
2604
2184
|
const value = params[key];
|
|
2605
2185
|
if (value === void 0 || value === null) return void 0;
|
|
2606
|
-
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`);
|
|
2607
2187
|
return value;
|
|
2608
2188
|
}
|
|
2609
2189
|
export {
|