@scp-dev/broker 0.1.3 → 0.1.5
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 +120 -516
- package/package.json +3 -3
- package/src/bridge.ts +1 -0
- package/src/client.ts +2 -2
- package/src/index.ts +3 -2
- package/src/judge-gate.ts +14 -545
- package/src/server.ts +75 -19
package/dist/index.js
CHANGED
|
@@ -732,7 +732,8 @@ function startBridge(options) {
|
|
|
732
732
|
labels: view.labels,
|
|
733
733
|
status: view.status ? { state: view.status.state, note: options.redact ? "" : view.status.note } : null,
|
|
734
734
|
presence: view.presence,
|
|
735
|
-
last_seen: view.last_seen
|
|
735
|
+
last_seen: view.last_seen,
|
|
736
|
+
...view.kind === "agent" ? { wake: view.wake ?? null } : {}
|
|
736
737
|
};
|
|
737
738
|
}
|
|
738
739
|
function localViews() {
|
|
@@ -1060,11 +1061,11 @@ import { setTimeout as sleep2 } from "timers/promises";
|
|
|
1060
1061
|
import {
|
|
1061
1062
|
BROKER_CAPABILITIES,
|
|
1062
1063
|
CLI_NAME,
|
|
1063
|
-
LIMITS
|
|
1064
|
+
LIMITS,
|
|
1064
1065
|
parsePolicy,
|
|
1065
1066
|
PREAMBLE,
|
|
1066
1067
|
PROTOCOL as PROTOCOL3,
|
|
1067
|
-
RelayError as
|
|
1068
|
+
RelayError as RelayError3,
|
|
1068
1069
|
SERVER_FIELDS,
|
|
1069
1070
|
stripControl
|
|
1070
1071
|
} from "@scp-dev/core";
|
|
@@ -1072,460 +1073,20 @@ import { FederatedCore } from "@scp-dev/hub";
|
|
|
1072
1073
|
|
|
1073
1074
|
// src/judge-gate.ts
|
|
1074
1075
|
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;
|
|
1076
|
+
import { JudgeGate as HubJudgeGate } from "@scp-dev/hub";
|
|
1077
|
+
function readKeyFile(file) {
|
|
1078
|
+
try {
|
|
1079
|
+
accessSync(file, constants.R_OK);
|
|
1080
|
+
} catch {
|
|
1081
|
+
return null;
|
|
1491
1082
|
}
|
|
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
|
-
};
|
|
1083
|
+
return async () => readFileSync4(file, "utf8").trim();
|
|
1084
|
+
}
|
|
1085
|
+
var JudgeGate = class extends HubJudgeGate {
|
|
1086
|
+
constructor(options) {
|
|
1087
|
+
super({ keyFile: readKeyFile, ...options });
|
|
1510
1088
|
}
|
|
1511
1089
|
};
|
|
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
1090
|
|
|
1530
1091
|
// src/sessions.ts
|
|
1531
1092
|
import { createHash as createHash2, randomUUID } from "crypto";
|
|
@@ -1650,7 +1211,19 @@ var SCOPE_METHODS = /* @__PURE__ */ new Set([
|
|
|
1650
1211
|
var CHANGES_SCOPE = /* @__PURE__ */ new Set(["rules_set", "rules_widen", "rules_lift", "group"]);
|
|
1651
1212
|
var ASK_RECHECK_MS = 1e3;
|
|
1652
1213
|
var STOP_RECHECK_MS = 1e3;
|
|
1653
|
-
var TRANSIENT_METHODS = /* @__PURE__ */ new Set([
|
|
1214
|
+
var TRANSIENT_METHODS = /* @__PURE__ */ new Set([
|
|
1215
|
+
"event",
|
|
1216
|
+
"status",
|
|
1217
|
+
"whoami",
|
|
1218
|
+
"inject",
|
|
1219
|
+
"injected",
|
|
1220
|
+
"peek_turn",
|
|
1221
|
+
"stop_wait",
|
|
1222
|
+
"prompted",
|
|
1223
|
+
"title_infer",
|
|
1224
|
+
"subagent_title",
|
|
1225
|
+
"wake"
|
|
1226
|
+
]);
|
|
1654
1227
|
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)`;
|
|
1655
1228
|
function workerResults(result) {
|
|
1656
1229
|
return result.messages.length - (result.others_shown ?? 0) + result.more;
|
|
@@ -1773,21 +1346,17 @@ async function startBroker(options) {
|
|
|
1773
1346
|
}
|
|
1774
1347
|
for (const id of known.keys()) if (!live.has(id)) known.delete(id);
|
|
1775
1348
|
}, options.presenceEveryMs ?? PRESENCE_MS);
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
log
|
|
1788
|
-
}
|
|
1789
|
-
});
|
|
1790
|
-
}
|
|
1349
|
+
const remoteHooks = {
|
|
1350
|
+
wake,
|
|
1351
|
+
fleet: notifyFleet,
|
|
1352
|
+
policyError: () => {
|
|
1353
|
+
loadPolicy();
|
|
1354
|
+
return policyError;
|
|
1355
|
+
},
|
|
1356
|
+
log
|
|
1357
|
+
};
|
|
1358
|
+
if (options.bridge) bridge = startBridge({ ...options.bridge, core, onRemote: remoteHooks });
|
|
1359
|
+
else if (options.link) bridge = options.link({ core, hooks: remoteHooks, send: (from, params, origin) => judge.send(from, params, origin, deliver) });
|
|
1791
1360
|
async function relink() {
|
|
1792
1361
|
const config = options.bridgeConfig ? await options.bridgeConfig() : options.bridge;
|
|
1793
1362
|
if (!config || bridge && config.url === options.bridge?.url && config.machine === options.bridge?.machine) {
|
|
@@ -1943,7 +1512,7 @@ async function startBroker(options) {
|
|
|
1943
1512
|
endpoint: core.whoami(endpoint),
|
|
1944
1513
|
capabilities: BROKER_CAPABILITIES,
|
|
1945
1514
|
server_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1946
|
-
limits:
|
|
1515
|
+
limits: LIMITS,
|
|
1947
1516
|
preamble: PREAMBLE,
|
|
1948
1517
|
broker: brokerInfo()
|
|
1949
1518
|
};
|
|
@@ -1973,16 +1542,16 @@ async function startBroker(options) {
|
|
|
1973
1542
|
}
|
|
1974
1543
|
async function gather(me, result, peek, s) {
|
|
1975
1544
|
const gathering = (r) => workerResults(r) > 0 && !r.more && !r.prompted && othersRunning(r) > 0;
|
|
1976
|
-
const cap = Date.now() +
|
|
1977
|
-
let quiet = Date.now() +
|
|
1545
|
+
const cap = Date.now() + LIMITS.pushGraceS * 1e3;
|
|
1546
|
+
let quiet = Date.now() + LIMITS.pushQuietS * 1e3;
|
|
1978
1547
|
let results = workerResults(result);
|
|
1979
1548
|
while (gathering(result) && !s.data.closed && Date.now() < Math.min(quiet, cap)) {
|
|
1980
1549
|
await waitFor(me, Math.min(quiet, cap) - Date.now(), s);
|
|
1981
|
-
if (closing) throw new
|
|
1550
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
1982
1551
|
result = core.stopWait(me, peek);
|
|
1983
1552
|
if (workerResults(result) > results) {
|
|
1984
1553
|
results = workerResults(result);
|
|
1985
|
-
quiet = Date.now() +
|
|
1554
|
+
quiet = Date.now() + LIMITS.pushQuietS * 1e3;
|
|
1986
1555
|
}
|
|
1987
1556
|
}
|
|
1988
1557
|
}
|
|
@@ -2076,7 +1645,7 @@ async function startBroker(options) {
|
|
|
2076
1645
|
if (hasId) write(s, { jsonrpc: "2.0", id, result });
|
|
2077
1646
|
} catch (error) {
|
|
2078
1647
|
if (!hasId || closing) return;
|
|
2079
|
-
if (error instanceof
|
|
1648
|
+
if (error instanceof RelayError3) {
|
|
2080
1649
|
const code = error.code === "method_not_found" ? -32601 : error.code === "invalid_params" ? -32602 : -32e3;
|
|
2081
1650
|
write(s, {
|
|
2082
1651
|
jsonrpc: "2.0",
|
|
@@ -2097,64 +1666,70 @@ async function startBroker(options) {
|
|
|
2097
1666
|
}
|
|
2098
1667
|
}
|
|
2099
1668
|
function endpointOf(s) {
|
|
2100
|
-
if (closing) throw new
|
|
1669
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
2101
1670
|
if (!s.data.endpoint)
|
|
2102
|
-
throw new
|
|
1671
|
+
throw new RelayError3("unauthorized", s.data.observer ? "this connection only observes: say hello as an agent or the human" : "call hello first");
|
|
2103
1672
|
core.touch(s.data.endpoint);
|
|
2104
1673
|
return s.data.endpoint;
|
|
2105
1674
|
}
|
|
2106
1675
|
function waitSeconds(value) {
|
|
2107
1676
|
if (value === void 0) return 0;
|
|
2108
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new
|
|
2109
|
-
return Math.min(value,
|
|
1677
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new RelayError3("invalid_params", "wait_s must be a number of seconds");
|
|
1678
|
+
return Math.min(value, LIMITS.waitMaxS) * 1e3;
|
|
2110
1679
|
}
|
|
2111
1680
|
function checkAttachments(attachments, s) {
|
|
2112
1681
|
if (attachments === void 0) return void 0;
|
|
2113
|
-
if (!Array.isArray(attachments)) throw new
|
|
1682
|
+
if (!Array.isArray(attachments)) throw new RelayError3("invalid_params", "attachments must be a list");
|
|
2114
1683
|
const tooWide = /* @__PURE__ */ new Set(["/", realOrSelf(homedir2()), realOrSelf(accountHome())]);
|
|
2115
1684
|
const roots = [s.data.cwd, ...options.roots ?? []].filter((r) => typeof r === "string" && isAbsolute(r)).map(realOrSelf).filter((r) => !tooWide.has(r));
|
|
2116
1685
|
return attachments.map((attachment) => {
|
|
2117
1686
|
if (attachment?.kind !== "path" || typeof attachment.path !== "string" || !isAbsolute(attachment.path)) {
|
|
2118
|
-
throw new
|
|
1687
|
+
throw new RelayError3("invalid_params", "an attachment is {kind: path, path: an absolute path}");
|
|
2119
1688
|
}
|
|
2120
1689
|
let real;
|
|
2121
1690
|
try {
|
|
2122
1691
|
real = realpathSync(attachment.path);
|
|
2123
1692
|
} catch {
|
|
2124
|
-
throw new
|
|
1693
|
+
throw new RelayError3("not_found", `no file at ${attachment.path}`);
|
|
2125
1694
|
}
|
|
2126
1695
|
if (!roots.some((root) => real === root || real.startsWith(root.endsWith(sep) ? root : root + sep))) {
|
|
2127
|
-
throw new
|
|
1696
|
+
throw new RelayError3("invalid_params", `${attachment.path} is outside the sender's working directory and the relay's allowed roots`);
|
|
2128
1697
|
}
|
|
2129
1698
|
return { kind: "path", path: real };
|
|
2130
1699
|
});
|
|
2131
1700
|
}
|
|
2132
1701
|
async function dispatch(s, method, params) {
|
|
2133
1702
|
if (method === "hello") {
|
|
2134
|
-
if (s.data.endpoint || s.data.observer) throw new
|
|
2135
|
-
if (params.protocol !== PROTOCOL3) throw new
|
|
1703
|
+
if (s.data.endpoint || s.data.observer) throw new RelayError3("invalid_params", "hello was already called on this connection");
|
|
1704
|
+
if (params.protocol !== PROTOCOL3) throw new RelayError3("invalid_params", `this broker speaks ${PROTOCOL3}`);
|
|
2136
1705
|
if (params.transient === true) {
|
|
2137
1706
|
if (params.human === true || params.observe === true || params.resume !== void 0) {
|
|
2138
|
-
throw new
|
|
1707
|
+
throw new RelayError3("invalid_params", "a hook connection claims an agent endpoint: transient does not go with human, observe or resume");
|
|
2139
1708
|
}
|
|
2140
1709
|
const join5 = params.join;
|
|
2141
1710
|
if (join5 !== void 0 && (typeof join5 !== "object" || join5 === null || Array.isArray(join5)))
|
|
2142
|
-
throw new
|
|
1711
|
+
throw new RelayError3("invalid_params", "join is {handle?, under?}");
|
|
1712
|
+
const under = join5?.under;
|
|
2143
1713
|
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 }) : {
|
|
1714
|
+
const { endpoint: endpoint2, subagent, created } = join5 === void 0 ? core.claim(params.claim ?? {}, { transient: true }) : {
|
|
1715
|
+
endpoint: core.joinSession(params.claim ?? {}, join5.handle, under),
|
|
1716
|
+
subagent: void 0,
|
|
1717
|
+
created: void 0
|
|
1718
|
+
};
|
|
2145
1719
|
s.data.endpoint = endpoint2.id;
|
|
2146
1720
|
s.data.transient = true;
|
|
2147
1721
|
const key = params.claim?.process;
|
|
2148
1722
|
if (typeof key === "string") s.data.process = key;
|
|
2149
1723
|
const was2 = before2.get(endpoint2.id)?.address;
|
|
2150
1724
|
if (join5 !== void 0) membership(endpoint2, core.sessionOf(endpoint2.id), true, was2?.slice(was2.lastIndexOf("/") + 1));
|
|
1725
|
+
if (under !== void 0 && before2.get(endpoint2.id)?.parent !== endpoint2.parent) bridge?.resync();
|
|
2151
1726
|
else if ((before2.get(endpoint2.id)?.presence ?? "offline") !== endpoint2.presence) presenceChanged(endpoint2);
|
|
2152
1727
|
return {
|
|
2153
1728
|
endpoint: endpoint2,
|
|
2154
1729
|
...subagent ? { subagent, ...created ? { created } : {} } : {},
|
|
2155
1730
|
capabilities: BROKER_CAPABILITIES,
|
|
2156
1731
|
server_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2157
|
-
limits:
|
|
1732
|
+
limits: LIMITS,
|
|
2158
1733
|
preamble: PREAMBLE,
|
|
2159
1734
|
broker: brokerInfo()
|
|
2160
1735
|
};
|
|
@@ -2162,7 +1737,7 @@ async function startBroker(options) {
|
|
|
2162
1737
|
if (params.observe === true) {
|
|
2163
1738
|
const self = params.self === void 0 ? null : core.placeLineage(params.self);
|
|
2164
1739
|
if (params.grant !== void 0) {
|
|
2165
|
-
if (!core.grantBinds(params.grant, self)) throw new
|
|
1740
|
+
if (!core.grantBinds(params.grant, self)) throw new RelayError3("unauthorized", GRANT_UNBOUND);
|
|
2166
1741
|
return { ...bindLineage(s, self, params, true), grant_bound: true };
|
|
2167
1742
|
}
|
|
2168
1743
|
if (self) return bindLineage(s, self, params, true);
|
|
@@ -2172,7 +1747,7 @@ async function startBroker(options) {
|
|
|
2172
1747
|
endpoint: null,
|
|
2173
1748
|
capabilities: BROKER_CAPABILITIES,
|
|
2174
1749
|
server_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2175
|
-
limits:
|
|
1750
|
+
limits: LIMITS,
|
|
2176
1751
|
preamble: PREAMBLE,
|
|
2177
1752
|
broker: brokerInfo()
|
|
2178
1753
|
};
|
|
@@ -2180,7 +1755,10 @@ async function startBroker(options) {
|
|
|
2180
1755
|
const claim = params.claim;
|
|
2181
1756
|
if (params.human === true && params.resume === void 0 && params.lineage !== void 0) {
|
|
2182
1757
|
const placed = core.placeLineage(params.lineage);
|
|
2183
|
-
if (placed)
|
|
1758
|
+
if (placed) {
|
|
1759
|
+
s.data.terminal = params.tty === true;
|
|
1760
|
+
return bindLineage(s, placed, params);
|
|
1761
|
+
}
|
|
2184
1762
|
} else if (params.human !== true && params.resume === void 0 && claim?.process === void 0 && typeof claim?.handle === "string") {
|
|
2185
1763
|
const placed = claim.lineage === void 0 ? null : core.placeLineage(claim.lineage, claim.handle);
|
|
2186
1764
|
if (placed && (claim.grant === void 0 || core.grantBinds(claim.grant, placed))) {
|
|
@@ -2215,17 +1793,17 @@ async function startBroker(options) {
|
|
|
2215
1793
|
...token ? { token } : {},
|
|
2216
1794
|
capabilities: BROKER_CAPABILITIES,
|
|
2217
1795
|
server_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2218
|
-
limits:
|
|
1796
|
+
limits: LIMITS,
|
|
2219
1797
|
preamble: PREAMBLE,
|
|
2220
1798
|
broker: brokerInfo()
|
|
2221
1799
|
};
|
|
2222
1800
|
}
|
|
2223
|
-
if (closing) throw new
|
|
1801
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
2224
1802
|
if (s.data.observer && OBSERVER_METHODS.has(method)) return observe(s, method, params);
|
|
2225
1803
|
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
|
|
1804
|
+
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
1805
|
const me = endpointOf(s);
|
|
2228
|
-
if (s.data.transient && !TRANSIENT_METHODS.has(method)) throw new
|
|
1806
|
+
if (s.data.transient && !TRANSIENT_METHODS.has(method)) throw new RelayError3("unauthorized", "a hook connection only posts activity and status");
|
|
2229
1807
|
if (OBSERVER_METHODS.has(method)) return observe(s, method, params);
|
|
2230
1808
|
if (SCOPE_METHODS.has(method)) return scope(s, method, params);
|
|
2231
1809
|
switch (method) {
|
|
@@ -2233,13 +1811,13 @@ async function startBroker(options) {
|
|
|
2233
1811
|
return s.data.transient ? core.whoami(me) : { ...core.whoami(me), ...core.peekState(me) };
|
|
2234
1812
|
case "peek_turn":
|
|
2235
1813
|
if (!s.data.transient || !core.hookOwns(me, s.data.process))
|
|
2236
|
-
throw new
|
|
1814
|
+
throw new RelayError3("unauthorized", "only a hook of the endpoint's own harness process posts digest turns");
|
|
2237
1815
|
return core.peekTurn(me, { role: params.role, text: params.text });
|
|
2238
1816
|
case "send":
|
|
2239
1817
|
case "ask": {
|
|
2240
|
-
for (const field of SERVER_FIELDS) if (field in params) throw new
|
|
1818
|
+
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
1819
|
loadPolicy();
|
|
2242
|
-
if (policyError) throw new
|
|
1820
|
+
if (policyError) throw new RelayError3("policy_refused", policyError);
|
|
2243
1821
|
const attachments = checkAttachments(params.attachments, s);
|
|
2244
1822
|
const send = { ...params, ...attachments ? { attachments } : {} };
|
|
2245
1823
|
if (method === "ask") send.kind = "request";
|
|
@@ -2255,16 +1833,16 @@ async function startBroker(options) {
|
|
|
2255
1833
|
case "hold_release": {
|
|
2256
1834
|
const id = required(reviewer(s, params), "id");
|
|
2257
1835
|
loadPolicy();
|
|
2258
|
-
if (policyError) throw new
|
|
1836
|
+
if (policyError) throw new RelayError3("policy_refused", policyError);
|
|
2259
1837
|
return judge.release(id, { redacted: params.redacted === true, by: core.addressOf(me) }, deliver);
|
|
2260
1838
|
}
|
|
2261
1839
|
case "hold_refuse": {
|
|
2262
1840
|
const reason = required(reviewer(s, params), "reason");
|
|
2263
|
-
if (reason.length >
|
|
1841
|
+
if (reason.length > LIMITS.noteChars) throw new RelayError3("invalid_params", `reason takes at most ${LIMITS.noteChars} characters`);
|
|
2264
1842
|
return judge.refuse(required(params, "id"), stripControl(reason), core.addressOf(me));
|
|
2265
1843
|
}
|
|
2266
1844
|
case "ask_wait": {
|
|
2267
|
-
if (typeof params.id !== "string") throw new
|
|
1845
|
+
if (typeof params.id !== "string") throw new RelayError3("invalid_params", "id is the request's message id");
|
|
2268
1846
|
return awaitReply(s, me, params.id, waitSeconds(params.wait_s));
|
|
2269
1847
|
}
|
|
2270
1848
|
case "inbox": {
|
|
@@ -2274,7 +1852,7 @@ async function startBroker(options) {
|
|
|
2274
1852
|
let page = read();
|
|
2275
1853
|
while (!page.messages.length && !s.data.closed && Date.now() < deadline) {
|
|
2276
1854
|
await waitFor(me, deadline - Date.now(), s);
|
|
2277
|
-
if (closing) throw new
|
|
1855
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
2278
1856
|
page = read();
|
|
2279
1857
|
}
|
|
2280
1858
|
const { messages } = page;
|
|
@@ -2292,11 +1870,37 @@ async function startBroker(options) {
|
|
|
2292
1870
|
if (changed) bridge?.changed(me);
|
|
2293
1871
|
return { changed };
|
|
2294
1872
|
}
|
|
1873
|
+
case "subagent_title": {
|
|
1874
|
+
const changed = core.subagentTitle(me, params, hookOf(s));
|
|
1875
|
+
if (changed) bridge?.changed(changed);
|
|
1876
|
+
return { changed: changed !== null };
|
|
1877
|
+
}
|
|
1878
|
+
case "wake": {
|
|
1879
|
+
if (s.data.lineage && !s.data.own) throw new RelayError3("unauthorized", "only the endpoint's own session says how it wakes");
|
|
1880
|
+
const changed = core.setWake(me, params.via, hookOf(s));
|
|
1881
|
+
if (changed) bridge?.changed(me);
|
|
1882
|
+
return { changed };
|
|
1883
|
+
}
|
|
2295
1884
|
case "rename": {
|
|
2296
1885
|
const renamed = core.rename(actorOf(s), { ...params.endpoint !== void 0 ? { endpoint: params.endpoint } : {}, handle: params.handle });
|
|
2297
1886
|
membership(renamed.endpoint, renamed.session, true, renamed.from);
|
|
2298
1887
|
return renamed.endpoint;
|
|
2299
1888
|
}
|
|
1889
|
+
case "adopt": {
|
|
1890
|
+
const actor = actorOf(s);
|
|
1891
|
+
const adopted = core.adopt(
|
|
1892
|
+
actor,
|
|
1893
|
+
{ endpoint: params.endpoint, ...params.parent !== void 0 ? { parent: params.parent } : {} },
|
|
1894
|
+
{ tty: actor.kind === "endpoint" && s.data.terminal === true }
|
|
1895
|
+
);
|
|
1896
|
+
bridge?.resync();
|
|
1897
|
+
return adopted;
|
|
1898
|
+
}
|
|
1899
|
+
case "unlink": {
|
|
1900
|
+
const unlinked = core.unlink(actorOf(s), params.endpoint !== void 0 ? { endpoint: params.endpoint } : {});
|
|
1901
|
+
bridge?.resync();
|
|
1902
|
+
return unlinked;
|
|
1903
|
+
}
|
|
2300
1904
|
case "leave": {
|
|
2301
1905
|
const left = core.leaveSession(actorOf(s), params.endpoint !== void 0 ? { endpoint: params.endpoint } : {});
|
|
2302
1906
|
for (const c of connections) if (c.data.endpoint === left.endpoint.id && !c.data.transient && !c.data.lineage && c !== s) c.end();
|
|
@@ -2332,7 +1936,7 @@ async function startBroker(options) {
|
|
|
2332
1936
|
let result = core.inject(me, options2);
|
|
2333
1937
|
while (!result.offered.length && !s.data.closed && Date.now() < deadline) {
|
|
2334
1938
|
await waitFor(me, deadline - Date.now(), s);
|
|
2335
|
-
if (closing) throw new
|
|
1939
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
2336
1940
|
result = core.inject(me, options2);
|
|
2337
1941
|
}
|
|
2338
1942
|
return result;
|
|
@@ -2346,7 +1950,7 @@ async function startBroker(options) {
|
|
|
2346
1950
|
const idle = (r) => !r.messages.length && !r.more && r.workers.length + r.starting > 0 && r.pushes_left > 0 && !r.prompted;
|
|
2347
1951
|
while (idle(result) && !s.data.closed && Date.now() < deadline) {
|
|
2348
1952
|
await waitFor(me, Math.min(STOP_RECHECK_MS, deadline - Date.now()), s);
|
|
2349
|
-
if (closing) throw new
|
|
1953
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
2350
1954
|
result = core.stopWait(me, peek);
|
|
2351
1955
|
}
|
|
2352
1956
|
if (waitMs > 0) await gather(me, result, peek, s);
|
|
@@ -2373,13 +1977,13 @@ async function startBroker(options) {
|
|
|
2373
1977
|
case "receipts":
|
|
2374
1978
|
return core.receipts(me, params.id);
|
|
2375
1979
|
}
|
|
2376
|
-
throw new
|
|
1980
|
+
throw new RelayError3("method_not_found", `no method ${method}`);
|
|
2377
1981
|
}
|
|
2378
1982
|
const deliver = async (from, send, origin) => {
|
|
2379
1983
|
let result;
|
|
2380
1984
|
if (remoteSend(core, send.to)) {
|
|
2381
1985
|
if (!bridge)
|
|
2382
|
-
throw new
|
|
1986
|
+
throw new RelayError3("not_found", `no endpoint ${[send.to].flat().join(", ")}: this broker is not linked to a hub (${CLI_NAME} cloud enable)`);
|
|
2383
1987
|
result = await bridge.forward(from, send, origin);
|
|
2384
1988
|
} else {
|
|
2385
1989
|
result = core.send(from, send, origin);
|
|
@@ -2399,7 +2003,7 @@ async function startBroker(options) {
|
|
|
2399
2003
|
return result;
|
|
2400
2004
|
};
|
|
2401
2005
|
function reviewer(s, params) {
|
|
2402
|
-
if (!atTerminal(s)) throw new
|
|
2006
|
+
if (!atTerminal(s)) throw new RelayError3("unauthorized", "only your human at a terminal reviews held messages");
|
|
2403
2007
|
return params;
|
|
2404
2008
|
}
|
|
2405
2009
|
function observe(s, method, params) {
|
|
@@ -2407,7 +2011,7 @@ async function startBroker(options) {
|
|
|
2407
2011
|
switch (method) {
|
|
2408
2012
|
case "list":
|
|
2409
2013
|
if (params.contacts === true) {
|
|
2410
|
-
if (!bridge) throw new
|
|
2014
|
+
if (!bridge) throw new RelayError3("unavailable", `this broker is not linked to a hub (${CLI_NAME} cloud enable), so it reaches no contacts`);
|
|
2411
2015
|
return bridge.contacts().then((contacts) => ({ contacts }));
|
|
2412
2016
|
}
|
|
2413
2017
|
return { endpoints: core.list({ ...params, viewer, peekable: viewer === void 0 }) };
|
|
@@ -2448,7 +2052,7 @@ async function startBroker(options) {
|
|
|
2448
2052
|
case "bridge_relink":
|
|
2449
2053
|
return relink();
|
|
2450
2054
|
}
|
|
2451
|
-
throw new
|
|
2055
|
+
throw new RelayError3("method_not_found", `no method ${method}`);
|
|
2452
2056
|
}
|
|
2453
2057
|
function scope(s, method, params) {
|
|
2454
2058
|
const result = scopeCall(s, method, params);
|
|
@@ -2527,7 +2131,7 @@ async function startBroker(options) {
|
|
|
2527
2131
|
return result;
|
|
2528
2132
|
}
|
|
2529
2133
|
}
|
|
2530
|
-
throw new
|
|
2134
|
+
throw new RelayError3("method_not_found", `no method ${method}`);
|
|
2531
2135
|
}
|
|
2532
2136
|
function realOrSelf(path) {
|
|
2533
2137
|
try {
|
|
@@ -2540,7 +2144,7 @@ async function startBroker(options) {
|
|
|
2540
2144
|
const deadline = Date.now() + ms;
|
|
2541
2145
|
const present = sent?.deliveries.filter((d) => !d.offline).map((d) => d.endpoint);
|
|
2542
2146
|
for (; ; ) {
|
|
2543
|
-
if (closing) throw new
|
|
2147
|
+
if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
|
|
2544
2148
|
const reply = core.replyTo(me, request);
|
|
2545
2149
|
if (reply) {
|
|
2546
2150
|
core.ack(me, [reply.seq]);
|
|
@@ -2592,18 +2196,18 @@ function compact(fields) {
|
|
|
2592
2196
|
function optional(params, key) {
|
|
2593
2197
|
const value = params[key];
|
|
2594
2198
|
if (value === void 0 || value === null) return void 0;
|
|
2595
|
-
if (typeof value !== "string") throw new
|
|
2199
|
+
if (typeof value !== "string") throw new RelayError3("invalid_params", `${key} must be a string`);
|
|
2596
2200
|
return value;
|
|
2597
2201
|
}
|
|
2598
2202
|
function required(params, key) {
|
|
2599
2203
|
const value = optional(params, key);
|
|
2600
|
-
if (!value) throw new
|
|
2204
|
+
if (!value) throw new RelayError3("invalid_params", `${key} is required`);
|
|
2601
2205
|
return value;
|
|
2602
2206
|
}
|
|
2603
2207
|
function selectors(params, key) {
|
|
2604
2208
|
const value = params[key];
|
|
2605
2209
|
if (value === void 0 || value === null) return void 0;
|
|
2606
|
-
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) throw new
|
|
2210
|
+
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) throw new RelayError3("invalid_params", `${key} must be a list of selectors`);
|
|
2607
2211
|
return value;
|
|
2608
2212
|
}
|
|
2609
2213
|
export {
|