@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.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 as LIMITS2,
1063
+ LIMITS,
1064
1064
  parsePolicy,
1065
1065
  PREAMBLE,
1066
1066
  PROTOCOL as PROTOCOL3,
1067
- RelayError as RelayError4,
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
- applyRedactions,
1077
- DEFAULT_JUDGE_BUDGET,
1078
- findSecret as findSecret2,
1079
- HttpJudgeClient,
1080
- JUDGE_PROMPT_VERSION,
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
- view(row) {
1493
- const params = JSON.parse(row.params);
1494
- const verdict = JSON.parse(row.verdict);
1495
- return {
1496
- id: row.id,
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
- if (options.bridge) {
1777
- bridge = startBridge({
1778
- ...options.bridge,
1779
- core,
1780
- onRemote: {
1781
- wake,
1782
- fleet: notifyFleet,
1783
- policyError: () => {
1784
- loadPolicy();
1785
- return policyError;
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: LIMITS2,
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() + LIMITS2.pushGraceS * 1e3;
1977
- let quiet = Date.now() + LIMITS2.pushQuietS * 1e3;
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 RelayError4("unauthorized", "the relay broker is stopping");
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() + LIMITS2.pushQuietS * 1e3;
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 RelayError4) {
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 RelayError4("unauthorized", "the relay broker is stopping");
1656
+ if (closing) throw new RelayError3("unauthorized", "the relay broker is stopping");
2101
1657
  if (!s.data.endpoint)
2102
- throw new RelayError4("unauthorized", s.data.observer ? "this connection only observes: say hello as an agent or the human" : "call hello first");
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 RelayError4("invalid_params", "wait_s must be a number of seconds");
2109
- return Math.min(value, LIMITS2.waitMaxS) * 1e3;
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 RelayError4("invalid_params", "attachments must be a list");
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 RelayError4("invalid_params", "an attachment is {kind: path, path: an absolute path}");
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 RelayError4("not_found", `no file at ${attachment.path}`);
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 RelayError4("invalid_params", `${attachment.path} is outside the sender's working directory and the relay's allowed roots`);
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 RelayError4("invalid_params", "hello was already called on this connection");
2135
- if (params.protocol !== PROTOCOL3) throw new RelayError4("invalid_params", `this broker speaks ${PROTOCOL3}`);
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 RelayError4("invalid_params", "a hook connection claims an agent endpoint: transient does not go with human, observe or resume");
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 RelayError4("invalid_params", "join is {handle?}");
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 }) : { endpoint: core.joinSession(params.claim ?? {}, join5.handle), subagent: void 0, created: void 0 };
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: LIMITS2,
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 RelayError4("unauthorized", GRANT_UNBOUND);
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: LIMITS2,
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) return bindLineage(s, placed, params);
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: LIMITS2,
1783
+ limits: LIMITS,
2219
1784
  preamble: PREAMBLE,
2220
1785
  broker: brokerInfo()
2221
1786
  };
2222
1787
  }
2223
- if (closing) throw new RelayError4("unauthorized", "the relay broker is stopping");
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 RelayError4("scope_refused", "a hook connection cannot read or change scopes, groups or grants");
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 RelayError4("unauthorized", "a hook connection only posts activity and status");
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 RelayError4("unauthorized", "only a hook of the endpoint's own harness process posts digest turns");
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 RelayError4("invalid_params", `${field} is set by the relay, not the sender`);
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 RelayError4("policy_refused", policyError);
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 RelayError4("policy_refused", policyError);
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 > LIMITS2.noteChars) throw new RelayError4("invalid_params", `reason takes at most ${LIMITS2.noteChars} characters`);
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 RelayError4("invalid_params", "id is the request's message id");
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 RelayError4("unauthorized", "the relay broker is stopping");
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 RelayError4("unauthorized", "the relay broker is stopping");
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 RelayError4("unauthorized", "the relay broker is stopping");
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 RelayError4("method_not_found", `no method ${method}`);
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 RelayError4("not_found", `no endpoint ${[send.to].flat().join(", ")}: this broker is not linked to a hub (${CLI_NAME} cloud enable)`);
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 RelayError4("unauthorized", "only your human at a terminal reviews held messages");
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 RelayError4("unavailable", `this broker is not linked to a hub (${CLI_NAME} cloud enable), so it reaches no contacts`);
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 RelayError4("method_not_found", `no method ${method}`);
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 RelayError4("method_not_found", `no method ${method}`);
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 RelayError4("unauthorized", "the relay broker is stopping");
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 RelayError4("invalid_params", `${key} must be a string`);
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 RelayError4("invalid_params", `${key} is required`);
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 RelayError4("invalid_params", `${key} must be a list of selectors`);
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 {