agents-city 0.4.0 → 0.5.2

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.
Files changed (98) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.es.md +240 -11
  3. package/README.md +238 -11
  4. package/benchmarks/latency/fake-claude-cli.mjs +2 -1
  5. package/benchmarks/reception/README.md +16 -0
  6. package/benchmarks/reception/run.mjs +84 -0
  7. package/bin/agents-city.js +1 -0
  8. package/bin/connect +5 -0
  9. package/bin/hall.html +117 -18
  10. package/bin/navegador.mjs +170 -1
  11. package/bin/serve.py +201 -101
  12. package/bin/test +15 -3
  13. package/bin/test-arnes.py +192 -0
  14. package/bin/test-cage.py +9 -0
  15. package/bin/test-channel.py +178 -12
  16. package/bin/test-claude-runtime.py +132 -4
  17. package/bin/test-connect-client.mjs +609 -0
  18. package/bin/test-connect.py +52 -0
  19. package/bin/test-contracts.py +214 -0
  20. package/bin/test-diario.py +185 -0
  21. package/bin/test-doctor.py +31 -0
  22. package/bin/test-i18n.py +189 -0
  23. package/bin/test-navegador.py +61 -0
  24. package/bin/test-seat.py +78 -20
  25. package/bin/test-security.py +2 -0
  26. package/bin/test-serve.py +366 -0
  27. package/city/web/dist-hall/hall.js +789 -214
  28. package/city/web/src/bienvenida.ts +14 -13
  29. package/city/web/src/casa.ts +143 -56
  30. package/city/web/src/demo.ts +61 -31
  31. package/city/web/src/es.ts +187 -27
  32. package/city/web/src/explorador.ts +70 -43
  33. package/city/web/src/hall.ts +520 -103
  34. package/city/web/src/idioma.ts +21 -1
  35. package/city/web/src/motores.ts +48 -1
  36. package/city/web/src/vista.ts +58 -0
  37. package/demo/graba.py +5 -10
  38. package/docs/managed-connect.md +237 -0
  39. package/docs/security.md +41 -2
  40. package/package.json +8 -4
  41. package/plugin/.claude-plugin/plugin.json +1 -1
  42. package/plugin/channel/adapter-prompts.ts +10 -0
  43. package/plugin/channel/adapter.js +78 -31
  44. package/plugin/channel/agents_city_hybrid_crypto_bg.wasm +0 -0
  45. package/plugin/channel/bus.js +109 -65
  46. package/plugin/channel/bus.ts +11 -1
  47. package/plugin/channel/client.js +67 -30
  48. package/plugin/channel/delivery-queue.ts +160 -21
  49. package/plugin/channel/hub/remote-roads.ts +32 -1
  50. package/plugin/channel/hub/road-controller.ts +116 -16
  51. package/plugin/channel/kinsh_vodozemac_wasm_bg.wasm +0 -0
  52. package/plugin/channel/licenses/CONNECT_CLIENT_THIRD_PARTY_NOTICES.md +21 -0
  53. package/plugin/channel/licenses/HYBRID_CRYPTO_THIRD_PARTY_NOTICES.md +12 -0
  54. package/plugin/channel/licenses/hybrid-crypto-Apache-2.0.txt +201 -0
  55. package/plugin/channel/licenses/keyring-MIT.txt +21 -0
  56. package/plugin/channel/licenses/vodozemac-Apache-2.0.txt +201 -0
  57. package/plugin/channel/local-hub.js +2080 -97
  58. package/plugin/channel/local-hub.ts +5 -4
  59. package/plugin/channel/managed-connect/bridge.ts +208 -0
  60. package/plugin/channel/managed-connect/cli.ts +416 -0
  61. package/plugin/channel/managed-connect/device.ts +15 -0
  62. package/plugin/channel/managed-connect/local-cities.ts +94 -0
  63. package/plugin/channel/managed-connect/person-message.ts +74 -0
  64. package/plugin/channel/managed-connect/reception-bridge.ts +341 -0
  65. package/plugin/channel/managed-connect/relay-session.ts +7 -0
  66. package/plugin/channel/managed-connect/storage.ts +653 -0
  67. package/plugin/channel/managed-connect/transport.ts +87 -0
  68. package/plugin/channel/managed-connect-cli.js +4777 -0
  69. package/plugin/channel/managed-connect-cli.ts +7 -0
  70. package/plugin/channel/managed-connect-client.d.ts +261 -0
  71. package/plugin/channel/managed-connect-client.js +6572 -0
  72. package/plugin/channel/managed-connect-client.manifest.json +38 -0
  73. package/plugin/channel/package-lock.json +123 -105
  74. package/plugin/channel/package.json +4 -4
  75. package/plugin/channel/protocol.ts +4 -0
  76. package/plugin/channel/reception.ts +896 -0
  77. package/plugin/channel/road-cli.ts +1 -1
  78. package/plugin/channel/runtime/arnes.json +189 -0
  79. package/plugin/channel/runtime/arnes.ts +47 -0
  80. package/plugin/channel/runtime/claude.ts +18 -0
  81. package/plugin/channel/runtime/codex-config.ts +51 -1
  82. package/plugin/channel/runtime/codex.ts +31 -11
  83. package/plugin/channel/runtime/kimi.ts +6 -4
  84. package/plugin/channel/runtime-files.ts +39 -5
  85. package/plugin/channel/runtime-gateway.js +426 -100
  86. package/plugin/channel/runtime-gateway.ts +12 -0
  87. package/plugin/channel/trust/agents-city-sandbox-roots.json +66 -0
  88. package/plugin/scripts/arnes.py +271 -0
  89. package/plugin/scripts/busca.py +46 -10
  90. package/plugin/scripts/cage.py +5 -0
  91. package/plugin/scripts/city-session.sh +144 -28
  92. package/plugin/scripts/crecimiento.py +4 -1
  93. package/plugin/scripts/demos.py +58 -43
  94. package/plugin/scripts/desinstala.py +36 -21
  95. package/plugin/scripts/diario.py +119 -0
  96. package/plugin/scripts/doctor.py +90 -15
  97. package/plugin/scripts/read-card.py +48 -8
  98. package/plugin/scripts/reception.py +665 -0
@@ -13,7 +13,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
13
13
  throw Error('Dynamic require of "' + x + '" is not supported');
14
14
  });
15
15
  var __commonJS = (cb, mod) => function __require2() {
16
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
16
+ try {
17
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
18
+ } catch (e) {
19
+ throw mod = 0, e;
20
+ }
17
21
  };
18
22
  var __copyProps = (to, from, except, desc) => {
19
23
  if (from && typeof from === "object" || typeof from === "function") {
@@ -45,10 +49,10 @@ var require_constants = __commonJS({
45
49
  EMPTY_BUFFER: Buffer.alloc(0),
46
50
  GUID: "258EAFA5-E914-47DA-95CA-C5AB0DC85B11",
47
51
  hasBlob,
48
- kForOnEventAttribute: Symbol("kIsForOnEventAttribute"),
49
- kListener: Symbol("kListener"),
50
- kStatusCode: Symbol("status-code"),
51
- kWebSocket: Symbol("websocket"),
52
+ kForOnEventAttribute: /* @__PURE__ */ Symbol("kIsForOnEventAttribute"),
53
+ kListener: /* @__PURE__ */ Symbol("kListener"),
54
+ kStatusCode: /* @__PURE__ */ Symbol("status-code"),
55
+ kWebSocket: /* @__PURE__ */ Symbol("websocket"),
52
56
  NOOP: () => {
53
57
  }
54
58
  };
@@ -134,8 +138,8 @@ var require_buffer_util = __commonJS({
134
138
  var require_limiter = __commonJS({
135
139
  "node_modules/ws/lib/limiter.js"(exports, module) {
136
140
  "use strict";
137
- var kDone = Symbol("kDone");
138
- var kRun = Symbol("kRun");
141
+ var kDone = /* @__PURE__ */ Symbol("kDone");
142
+ var kRun = /* @__PURE__ */ Symbol("kRun");
139
143
  var Limiter = class {
140
144
  /**
141
145
  * Creates a new `Limiter`.
@@ -190,11 +194,11 @@ var require_permessage_deflate = __commonJS({
190
194
  var { kStatusCode } = require_constants();
191
195
  var FastBuffer = Buffer[Symbol.species];
192
196
  var TRAILER = Buffer.from([0, 0, 255, 255]);
193
- var kPerMessageDeflate = Symbol("permessage-deflate");
194
- var kTotalLength = Symbol("total-length");
195
- var kCallback = Symbol("callback");
196
- var kBuffers = Symbol("buffers");
197
- var kError = Symbol("error");
197
+ var kPerMessageDeflate = /* @__PURE__ */ Symbol("permessage-deflate");
198
+ var kTotalLength = /* @__PURE__ */ Symbol("total-length");
199
+ var kCallback = /* @__PURE__ */ Symbol("callback");
200
+ var kBuffers = /* @__PURE__ */ Symbol("buffers");
201
+ var kError = /* @__PURE__ */ Symbol("error");
198
202
  var zlibLimiter;
199
203
  var PerMessageDeflate2 = class {
200
204
  /**
@@ -1400,7 +1404,7 @@ var require_sender = __commonJS({
1400
1404
  var { EMPTY_BUFFER, kWebSocket, NOOP } = require_constants();
1401
1405
  var { isBlob, isValidStatusCode } = require_validation();
1402
1406
  var { mask: applyMask, toBuffer } = require_buffer_util();
1403
- var kByteLength = Symbol("kByteLength");
1407
+ var kByteLength = /* @__PURE__ */ Symbol("kByteLength");
1404
1408
  var maskBuffer = Buffer.alloc(4);
1405
1409
  var RANDOM_POOL_SIZE = 8 * 1024;
1406
1410
  var randomPool;
@@ -1885,14 +1889,14 @@ var require_event_target = __commonJS({
1885
1889
  "node_modules/ws/lib/event-target.js"(exports, module) {
1886
1890
  "use strict";
1887
1891
  var { kForOnEventAttribute, kListener } = require_constants();
1888
- var kCode = Symbol("kCode");
1889
- var kData = Symbol("kData");
1890
- var kError = Symbol("kError");
1891
- var kMessage = Symbol("kMessage");
1892
- var kReason = Symbol("kReason");
1893
- var kTarget = Symbol("kTarget");
1894
- var kType = Symbol("kType");
1895
- var kWasClean = Symbol("kWasClean");
1892
+ var kCode = /* @__PURE__ */ Symbol("kCode");
1893
+ var kData = /* @__PURE__ */ Symbol("kData");
1894
+ var kError = /* @__PURE__ */ Symbol("kError");
1895
+ var kMessage = /* @__PURE__ */ Symbol("kMessage");
1896
+ var kReason = /* @__PURE__ */ Symbol("kReason");
1897
+ var kTarget = /* @__PURE__ */ Symbol("kTarget");
1898
+ var kType = /* @__PURE__ */ Symbol("kType");
1899
+ var kWasClean = /* @__PURE__ */ Symbol("kWasClean");
1896
1900
  var Event = class {
1897
1901
  /**
1898
1902
  * Create a new `Event`.
@@ -2271,7 +2275,7 @@ var require_websocket = __commonJS({
2271
2275
  var http = __require("http");
2272
2276
  var net = __require("net");
2273
2277
  var tls = __require("tls");
2274
- var { randomBytes: randomBytes2, createHash } = __require("crypto");
2278
+ var { randomBytes: randomBytes2, createHash: createHash3 } = __require("crypto");
2275
2279
  var { Duplex, Readable } = __require("stream");
2276
2280
  var { URL: URL2 } = __require("url");
2277
2281
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -2294,7 +2298,7 @@ var require_websocket = __commonJS({
2294
2298
  } = require_event_target();
2295
2299
  var { format, parse } = require_extension();
2296
2300
  var { toBuffer } = require_buffer_util();
2297
- var kAborted = Symbol("kAborted");
2301
+ var kAborted = /* @__PURE__ */ Symbol("kAborted");
2298
2302
  var protocolVersions = [8, 13];
2299
2303
  var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
2300
2304
  var subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
@@ -2939,7 +2943,7 @@ var require_websocket = __commonJS({
2939
2943
  abortHandshake(websocket2, socket, "Invalid Upgrade header");
2940
2944
  return;
2941
2945
  }
2942
- const digest = createHash("sha1").update(key + GUID).digest("base64");
2946
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
2943
2947
  if (res.headers["sec-websocket-accept"] !== digest) {
2944
2948
  abortHandshake(websocket2, socket, "Invalid Sec-WebSocket-Accept header");
2945
2949
  return;
@@ -3308,7 +3312,7 @@ var require_websocket_server = __commonJS({
3308
3312
  var EventEmitter = __require("events");
3309
3313
  var http = __require("http");
3310
3314
  var { Duplex } = __require("stream");
3311
- var { createHash } = __require("crypto");
3315
+ var { createHash: createHash3 } = __require("crypto");
3312
3316
  var extension2 = require_extension();
3313
3317
  var PerMessageDeflate2 = require_permessage_deflate();
3314
3318
  var subprotocol2 = require_subprotocol();
@@ -3615,7 +3619,7 @@ var require_websocket_server = __commonJS({
3615
3619
  );
3616
3620
  }
3617
3621
  if (this._state > RUNNING) return abortHandshake(socket, 503);
3618
- const digest = createHash("sha1").update(key + GUID).digest("base64");
3622
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
3619
3623
  const headers = [
3620
3624
  "HTTP/1.1 101 Switching Protocols",
3621
3625
  "Upgrade: websocket",
@@ -3705,7 +3709,7 @@ var require_websocket_server = __commonJS({
3705
3709
  // local-hub.ts
3706
3710
  import { timingSafeEqual } from "crypto";
3707
3711
  import { createServer } from "http";
3708
- import { readFileSync as readFileSync8 } from "fs";
3712
+ import { readFileSync as readFileSync10 } from "fs";
3709
3713
 
3710
3714
  // node_modules/ws/wrapper.mjs
3711
3715
  var import_stream = __toESM(require_stream(), 1);
@@ -4546,22 +4550,63 @@ function escapeRegExp(value) {
4546
4550
  }
4547
4551
 
4548
4552
  // delivery-queue.ts
4549
- import { appendFileSync as appendFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, unlinkSync } from "fs";
4553
+ import {
4554
+ appendFileSync as appendFileSync2,
4555
+ existsSync as existsSync4,
4556
+ mkdirSync as mkdirSync3,
4557
+ readFileSync as readFileSync4,
4558
+ readdirSync as readdirSync2,
4559
+ statSync,
4560
+ unlinkSync as unlinkSync2
4561
+ } from "fs";
4550
4562
  import { join as join5 } from "path";
4551
4563
 
4552
4564
  // runtime-files.ts
4553
4565
  import { randomBytes } from "crypto";
4554
- import { chmodSync as chmodSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "fs";
4566
+ import {
4567
+ chmodSync as chmodSync2,
4568
+ closeSync,
4569
+ constants,
4570
+ existsSync as existsSync3,
4571
+ fsyncSync,
4572
+ mkdirSync as mkdirSync2,
4573
+ openSync,
4574
+ readFileSync as readFileSync3,
4575
+ renameSync as renameSync3,
4576
+ unlinkSync,
4577
+ writeFileSync as writeFileSync3
4578
+ } from "fs";
4555
4579
  import { dirname, join as join4 } from "path";
4556
4580
  var counter = 0;
4557
4581
  function atomicJson(path, value) {
4558
- mkdirSync2(dirname(path), { recursive: true, mode: 448 });
4582
+ const directory = dirname(path);
4583
+ mkdirSync2(directory, { recursive: true, mode: 448 });
4559
4584
  const tmp = `${path}.tmp-${process.pid}-${counter++}`;
4560
- writeFileSync3(tmp, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
4561
- renameSync3(tmp, path);
4562
4585
  try {
4586
+ const fd = openSync(tmp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 384);
4587
+ try {
4588
+ writeFileSync3(fd, JSON.stringify(value, null, 2) + "\n");
4589
+ fsyncSync(fd);
4590
+ } finally {
4591
+ closeSync(fd);
4592
+ }
4593
+ renameSync3(tmp, path);
4563
4594
  chmodSync2(path, 384);
4564
- } catch {
4595
+ try {
4596
+ const dirFd = openSync(directory, constants.O_RDONLY);
4597
+ try {
4598
+ fsyncSync(dirFd);
4599
+ } finally {
4600
+ closeSync(dirFd);
4601
+ }
4602
+ } catch {
4603
+ }
4604
+ } catch (error) {
4605
+ try {
4606
+ unlinkSync(tmp);
4607
+ } catch {
4608
+ }
4609
+ throw error;
4565
4610
  }
4566
4611
  }
4567
4612
  function actorCredential(context2, actor) {
@@ -4604,11 +4649,16 @@ function endpointPath(context2) {
4604
4649
  }
4605
4650
 
4606
4651
  // delivery-queue.ts
4652
+ var ROAD_INBOX_BATCH_SIZE = 20;
4653
+ var DEFAULT_ROAD_INBOX_LIMIT = 500;
4654
+ var MAX_ROAD_INBOX_LIMIT = 1e4;
4655
+ var ROAD_INBOX_WAKE_FILE = "road-inbox-wakeup.json";
4607
4656
  function enqueueForActor(runtimeDir, envelope) {
4608
4657
  const directory = join5(runtimeDir, "outbox", safeSegment(envelope.to.actor));
4609
4658
  mkdirSync3(directory, { recursive: true, mode: 448 });
4610
- trim(directory);
4611
- atomicJson(join5(directory, `${fileKey(envelope.id)}.json`), envelope);
4659
+ const path = join5(directory, `${fileKey(envelope.id)}.json`);
4660
+ requireCapacity(directory, path, MAX_PENDING, "actor_outbox_full");
4661
+ atomicJson(path, envelope);
4612
4662
  }
4613
4663
  function pendingForActor(runtimeDir, actor) {
4614
4664
  const directory = join5(runtimeDir, "outbox", safeSegment(actor));
@@ -4617,13 +4667,13 @@ function pendingForActor(runtimeDir, actor) {
4617
4667
  try {
4618
4668
  const envelope = JSON.parse(readFileSync4(path, "utf8"));
4619
4669
  if (now - Date.parse(envelope.createdAt) > MESSAGE_TTL_MS) {
4620
- unlinkSync(path);
4670
+ unlinkSync2(path);
4621
4671
  return null;
4622
4672
  }
4623
4673
  return envelope;
4624
4674
  } catch {
4625
4675
  try {
4626
- unlinkSync(path);
4676
+ unlinkSync2(path);
4627
4677
  } catch {
4628
4678
  }
4629
4679
  return null;
@@ -4633,7 +4683,7 @@ function pendingForActor(runtimeDir, actor) {
4633
4683
  function acknowledge(runtimeDir, actor, envelopeId) {
4634
4684
  const path = join5(runtimeDir, "outbox", safeSegment(actor), `${fileKey(envelopeId)}.json`);
4635
4685
  try {
4636
- unlinkSync(path);
4686
+ unlinkSync2(path);
4637
4687
  return true;
4638
4688
  } catch {
4639
4689
  return false;
@@ -4642,48 +4692,107 @@ function acknowledge(runtimeDir, actor, envelopeId) {
4642
4692
  function queueRoad(runtimeDir, envelope) {
4643
4693
  const directory = join5(runtimeDir, "road-queue");
4644
4694
  mkdirSync3(directory, { recursive: true, mode: 448 });
4645
- trim(directory);
4646
- atomicJson(join5(directory, `${fileKey(envelope.id)}.json`), envelope);
4695
+ const path = join5(directory, `${fileKey(envelope.id)}.json`);
4696
+ requireCapacity(directory, path, MAX_PENDING, "road_queue_full");
4697
+ atomicJson(path, envelope);
4647
4698
  }
4648
- function drainRoadQueue(runtimeDir) {
4699
+ function pendingRoadQueue(runtimeDir) {
4649
4700
  const directory = join5(runtimeDir, "road-queue");
4650
4701
  const out = [];
4651
- for (const path of jsonFiles(directory)) {
4702
+ for (const path of jsonFilesByAge(directory)) {
4652
4703
  try {
4653
4704
  const envelope = JSON.parse(readFileSync4(path, "utf8"));
4654
- if (Date.now() - Date.parse(envelope.createdAt) <= MESSAGE_TTL_MS) out.push(envelope);
4705
+ if (Date.now() - Date.parse(envelope.createdAt) <= MESSAGE_TTL_MS) {
4706
+ out.push({ envelope, queueFile: path });
4707
+ continue;
4708
+ }
4655
4709
  } catch {
4656
4710
  }
4657
4711
  try {
4658
- unlinkSync(path);
4712
+ unlinkSync2(path);
4659
4713
  } catch {
4660
4714
  }
4661
4715
  }
4662
4716
  return out;
4663
4717
  }
4718
+ function acknowledgeRoadQueue(queueFile) {
4719
+ try {
4720
+ unlinkSync2(queueFile);
4721
+ } catch {
4722
+ }
4723
+ }
4664
4724
  function recordRoadInbox(runtimeDir, envelope) {
4665
4725
  const directory = join5(runtimeDir, "road-inbox");
4726
+ const receipts = join5(runtimeDir, "road-receipts");
4666
4727
  mkdirSync3(directory, { recursive: true, mode: 448 });
4667
- trim(directory);
4668
- atomicJson(join5(directory, `${fileKey(envelope.id)}.json`), envelope);
4669
- appendFileSync2(join5(runtimeDir, "road-history.jsonl"), JSON.stringify(envelope) + "\n", {
4670
- mode: 384
4671
- });
4728
+ mkdirSync3(receipts, { recursive: true, mode: 448 });
4729
+ const key = fileKey(envelope.id);
4730
+ const receipt = join5(receipts, `${key}.json`);
4731
+ if (existsSync4(receipt)) return false;
4732
+ const inbox = join5(directory, `${key}.json`);
4733
+ const recovered = existsSync4(inbox);
4734
+ if (!recovered) {
4735
+ requireCapacity(directory, inbox, roadInboxLimit(), "road_inbox_full");
4736
+ atomicJson(inbox, envelope);
4737
+ appendFileSync2(join5(runtimeDir, "road-history.jsonl"), JSON.stringify(envelope) + "\n", {
4738
+ mode: 384
4739
+ });
4740
+ }
4741
+ return true;
4672
4742
  }
4673
- function takeRoadInbox(runtimeDir) {
4743
+ function markRoadInboxAccepted(runtimeDir, envelopeId) {
4744
+ const receipts = join5(runtimeDir, "road-receipts");
4745
+ mkdirSync3(receipts, { recursive: true, mode: 448 });
4746
+ const key = fileKey(envelopeId);
4747
+ const receipt = join5(receipts, `${key}.json`);
4748
+ if (existsSync4(receipt)) return;
4749
+ trimTo(receipts, 1e3);
4750
+ atomicJson(receipt, { id: envelopeId, acceptedAt: (/* @__PURE__ */ new Date()).toISOString() });
4751
+ }
4752
+ function roadInboxStatus(runtimeDir) {
4753
+ const directory = join5(runtimeDir, "road-inbox");
4754
+ const paths = jsonFilesByAge(directory);
4755
+ let notifiedAt = 0;
4756
+ try {
4757
+ const state = JSON.parse(readFileSync4(join5(runtimeDir, ROAD_INBOX_WAKE_FILE), "utf8"));
4758
+ if (Number.isSafeInteger(state.notifiedAt) && Number(state.notifiedAt) > 0) {
4759
+ notifiedAt = Number(state.notifiedAt);
4760
+ }
4761
+ } catch {
4762
+ }
4763
+ return {
4764
+ pending: paths.length,
4765
+ oldestAt: oldestTimestamp(paths),
4766
+ notifiedAt
4767
+ };
4768
+ }
4769
+ function markRoadInboxNotified(runtimeDir, notifiedAt = Date.now()) {
4770
+ atomicJson(join5(runtimeDir, ROAD_INBOX_WAKE_FILE), { notifiedAt });
4771
+ }
4772
+ function takeRoadInbox(runtimeDir, limit = ROAD_INBOX_BATCH_SIZE) {
4773
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > ROAD_INBOX_BATCH_SIZE) {
4774
+ throw new Error("invalid_road_inbox_batch_size");
4775
+ }
4674
4776
  const directory = join5(runtimeDir, "road-inbox");
4675
4777
  const out = [];
4676
- for (const path of jsonFiles(directory)) {
4778
+ for (const path of jsonFilesByAge(directory).slice(0, limit)) {
4677
4779
  try {
4678
4780
  out.push(JSON.parse(readFileSync4(path, "utf8")));
4679
4781
  } catch {
4680
4782
  }
4681
4783
  try {
4682
- unlinkSync(path);
4784
+ unlinkSync2(path);
4683
4785
  } catch {
4684
4786
  }
4685
4787
  }
4686
- return out;
4788
+ const remaining = jsonFiles(directory).length;
4789
+ if (!remaining) {
4790
+ try {
4791
+ unlinkSync2(join5(runtimeDir, ROAD_INBOX_WAKE_FILE));
4792
+ } catch {
4793
+ }
4794
+ }
4795
+ return { messages: out, remaining };
4687
4796
  }
4688
4797
  function jsonFiles(directory) {
4689
4798
  if (!existsSync4(directory)) return [];
@@ -4693,16 +4802,50 @@ function jsonFiles(directory) {
4693
4802
  return [];
4694
4803
  }
4695
4804
  }
4805
+ function jsonFilesByAge(directory) {
4806
+ return jsonFiles(directory).sort((left, right) => {
4807
+ try {
4808
+ const delta = statSync(left).mtimeMs - statSync(right).mtimeMs;
4809
+ return delta || left.localeCompare(right);
4810
+ } catch {
4811
+ return left.localeCompare(right);
4812
+ }
4813
+ });
4814
+ }
4815
+ function oldestTimestamp(paths) {
4816
+ for (const path of paths) {
4817
+ try {
4818
+ return new Date(statSync(path).mtimeMs).toISOString();
4819
+ } catch {
4820
+ }
4821
+ }
4822
+ return null;
4823
+ }
4696
4824
  function fileKey(value) {
4697
4825
  const out = String(value).replace(/[^a-zA-Z0-9_.-]+/g, "-").slice(0, 160);
4698
4826
  if (!out) throw new Error("invalid message id");
4699
4827
  return out;
4700
4828
  }
4701
- function trim(directory) {
4702
- const files2 = jsonFiles(directory);
4703
- for (const path of files2.slice(0, Math.max(0, files2.length - MAX_PENDING + 1))) {
4829
+ function roadInboxLimit() {
4830
+ const configured = Number(process.env.CITY_ROAD_INBOX_MAX_PENDING);
4831
+ return Number.isSafeInteger(configured) && configured >= ROAD_INBOX_BATCH_SIZE ? Math.min(configured, MAX_ROAD_INBOX_LIMIT) : DEFAULT_ROAD_INBOX_LIMIT;
4832
+ }
4833
+ function requireCapacity(directory, target, maximum, code) {
4834
+ if (existsSync4(target)) return;
4835
+ if (jsonFiles(directory).length >= maximum) throw new Error(code);
4836
+ }
4837
+ function trimTo(directory, maximum) {
4838
+ const files2 = jsonFiles(directory).sort((left, right) => {
4839
+ try {
4840
+ const delta = statSync(left).mtimeMs - statSync(right).mtimeMs;
4841
+ return delta || left.localeCompare(right);
4842
+ } catch {
4843
+ return left.localeCompare(right);
4844
+ }
4845
+ });
4846
+ for (const path of files2.slice(0, Math.max(0, files2.length - maximum + 1))) {
4704
4847
  try {
4705
- unlinkSync(path);
4848
+ unlinkSync2(path);
4706
4849
  } catch {
4707
4850
  }
4708
4851
  }
@@ -4962,7 +5105,7 @@ import {
4962
5105
  mkdirSync as mkdirSync4,
4963
5106
  readFileSync as readFileSync5,
4964
5107
  renameSync as renameSync4,
4965
- statSync,
5108
+ statSync as statSync2,
4966
5109
  writeFileSync as writeFileSync4
4967
5110
  } from "fs";
4968
5111
  import { join as join6 } from "path";
@@ -5049,7 +5192,7 @@ function readRecent(path, limit) {
5049
5192
  }
5050
5193
  function fileIsLarge(path) {
5051
5194
  try {
5052
- return statSync(path).size > 2e6;
5195
+ return statSync2(path).size > 2e6;
5053
5196
  } catch {
5054
5197
  return false;
5055
5198
  }
@@ -5308,12 +5451,12 @@ function envelopeRouter(context2, connections2, options = {}) {
5308
5451
 
5309
5452
  // hub/lifecycle.ts
5310
5453
  import {
5311
- closeSync,
5454
+ closeSync as closeSync2,
5312
5455
  mkdirSync as mkdirSync6,
5313
- openSync,
5456
+ openSync as openSync2,
5314
5457
  readFileSync as readFileSync6,
5315
- statSync as statSync2,
5316
- unlinkSync as unlinkSync2,
5458
+ statSync as statSync3,
5459
+ unlinkSync as unlinkSync3,
5317
5460
  writeFileSync as writeFileSync5
5318
5461
  } from "fs";
5319
5462
  import { join as join8 } from "path";
@@ -5324,11 +5467,11 @@ function acquireHub(context2) {
5324
5467
  let acquired = false;
5325
5468
  for (let attempt = 0; attempt < 2 && !acquired; attempt += 1) {
5326
5469
  try {
5327
- const fd = openSync(lock, "wx", 384);
5470
+ const fd = openSync2(lock, "wx", 384);
5328
5471
  try {
5329
5472
  writeFileSync5(fd, String(owner) + "\n");
5330
5473
  } finally {
5331
- closeSync(fd);
5474
+ closeSync2(fd);
5332
5475
  }
5333
5476
  acquired = true;
5334
5477
  } catch (error) {
@@ -5342,7 +5485,7 @@ function acquireHub(context2) {
5342
5485
  throw new Error("city bus is already starting");
5343
5486
  }
5344
5487
  try {
5345
- unlinkSync2(lock);
5488
+ unlinkSync3(lock);
5346
5489
  } catch (unlinkError) {
5347
5490
  if (unlinkError.code !== "ENOENT") throw unlinkError;
5348
5491
  }
@@ -5354,11 +5497,11 @@ function acquireHub(context2) {
5354
5497
  const endpoint = endpointPath(context2);
5355
5498
  try {
5356
5499
  const published = JSON.parse(readFileSync6(endpoint, "utf8"));
5357
- if (published.pid === owner) unlinkSync2(endpoint);
5500
+ if (published.pid === owner) unlinkSync3(endpoint);
5358
5501
  } catch {
5359
5502
  }
5360
5503
  try {
5361
- unlinkSync2(lock);
5504
+ unlinkSync3(lock);
5362
5505
  } catch {
5363
5506
  }
5364
5507
  };
@@ -5372,7 +5515,7 @@ function lockOwner(path) {
5372
5515
  }
5373
5516
  function lockIsFresh(path) {
5374
5517
  try {
5375
- return Date.now() - statSync2(path).mtimeMs < 5e3;
5518
+ return Date.now() - statSync3(path).mtimeMs < 5e3;
5376
5519
  } catch {
5377
5520
  return false;
5378
5521
  }
@@ -5389,6 +5532,12 @@ function publishEndpoint(context2, endpoint) {
5389
5532
  atomicJson(endpointPath(context2), endpoint);
5390
5533
  }
5391
5534
 
5535
+ // reception.ts
5536
+ import { createHash } from "node:crypto";
5537
+ import { chmodSync as chmodSync3, existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync7, realpathSync } from "node:fs";
5538
+ import { join as join9, resolve as resolve2 } from "node:path";
5539
+ import { DatabaseSync } from "node:sqlite";
5540
+
5392
5541
  // untrusted.ts
5393
5542
  var SPECIAL_TOKEN = /<\|[a-zA-Z0-9_]+\|>|<\/?s>|\[INST\]|\[\/INST\]|<<SYS>>|<<\/SYS>>|<start_of_turn>|<end_of_turn>/g;
5394
5543
  function stripSpecialTokens(input) {
@@ -5407,15 +5556,691 @@ ${cleanBody}
5407
5556
  ${close2}`, markerId };
5408
5557
  }
5409
5558
 
5559
+ // reception.ts
5560
+ var RECEPTION_PROTOCOL = "agents-city-reception/1";
5561
+ var RECEPTION_SCHEMA_VERSION = 3;
5562
+ var AUTO_ROUTER_PROFILE = "deterministic-rules/1";
5563
+ var DEFAULT_PENDING_MESSAGES = 1e4;
5564
+ var MAX_PENDING_MESSAGES = 1e5;
5565
+ var DEFAULT_PENDING_BYTES = 64 * 1024 * 1024;
5566
+ var MAX_PENDING_BYTES = 512 * 1024 * 1024;
5567
+ var DELIVERY_BATCH = 20;
5568
+ var databases = /* @__PURE__ */ new Map();
5569
+ function receptionDatabasePath(appHome) {
5570
+ return join9(resolve2(appHome), ".runtime", "reception", "reception.sqlite3");
5571
+ }
5572
+ function recordReceptionMessage(context2, envelope) {
5573
+ return recordReceptionMessages(context2, [envelope]);
5574
+ }
5575
+ function recordReceptionMessages(context2, envelopes) {
5576
+ if (!envelopes.length || envelopes.length > 32) {
5577
+ throw new Error("invalid_reception_message_batch");
5578
+ }
5579
+ const rows = envelopes.map(validateReceptionEnvelope);
5580
+ if (new Set(rows.map((row) => row.envelope.id)).size !== rows.length) {
5581
+ throw new Error("duplicate_reception_message_batch");
5582
+ }
5583
+ const database = receptionDatabase(context2.appHome);
5584
+ const maximumMessages = boundedInteger(
5585
+ process.env.CITY_RECEPTION_MAX_PENDING,
5586
+ DEFAULT_PENDING_MESSAGES,
5587
+ 100,
5588
+ MAX_PENDING_MESSAGES
5589
+ );
5590
+ const maximumBytes = boundedInteger(
5591
+ process.env.CITY_RECEPTION_MAX_BYTES,
5592
+ DEFAULT_PENDING_BYTES,
5593
+ 1024 * 1024,
5594
+ MAX_PENDING_BYTES
5595
+ );
5596
+ let committed = false;
5597
+ try {
5598
+ database.exec("BEGIN IMMEDIATE");
5599
+ expireOldReception(database);
5600
+ const fresh = rows.filter((row) => {
5601
+ const existing = database.prepare("SELECT 1 AS found FROM reception_messages WHERE message_id = ? LIMIT 1").get(row.envelope.id);
5602
+ return !existing;
5603
+ });
5604
+ if (!fresh.length) {
5605
+ const current = receptionCounters(database);
5606
+ database.exec("COMMIT");
5607
+ committed = true;
5608
+ return { inserted: false, ...current };
5609
+ }
5610
+ const counters = receptionCounters(database);
5611
+ const bytes = fresh.reduce((sum, row) => sum + row.bytes, 0);
5612
+ if (counters.pending + fresh.length > maximumMessages || counters.pendingBytes + bytes > maximumBytes) {
5613
+ throw new Error("reception_inbox_full");
5614
+ }
5615
+ const insert = database.prepare(`
5616
+ INSERT INTO reception_messages (
5617
+ message_id, protocol, state, source_city, source_created_at,
5618
+ source_name, message_kind, in_reply_to,
5619
+ received_city_id, received_city_address, body, body_sha256,
5620
+ connection_id, road_id, remote_message_id, received_at
5621
+ ) VALUES (?, ?, 'pending', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5622
+ `);
5623
+ const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
5624
+ for (const row of fresh) {
5625
+ const envelope = row.envelope;
5626
+ insert.run(
5627
+ envelope.id,
5628
+ RECEPTION_PROTOCOL,
5629
+ envelope.from.city,
5630
+ envelope.createdAt,
5631
+ row.sourceName,
5632
+ row.kind,
5633
+ row.inReplyTo,
5634
+ context2.city.id,
5635
+ context2.city.address,
5636
+ row.body,
5637
+ sha256(row.body),
5638
+ optionalText(envelope.payload?.connectionId, 160),
5639
+ optionalText(envelope.payload?.roadId, 160),
5640
+ optionalText(envelope.payload?.remoteMessageId, 160),
5641
+ receivedAt
5642
+ );
5643
+ }
5644
+ applyAutomaticRouting(
5645
+ database,
5646
+ fresh.map((row) => row.envelope.id)
5647
+ );
5648
+ const updated = receptionCounters(database);
5649
+ database.exec("COMMIT");
5650
+ committed = true;
5651
+ return { inserted: true, ...updated };
5652
+ } finally {
5653
+ if (!committed) {
5654
+ try {
5655
+ database.exec("ROLLBACK");
5656
+ } catch {
5657
+ }
5658
+ }
5659
+ }
5660
+ }
5661
+ function validateReceptionEnvelope(envelope) {
5662
+ const body = envelope.payload?.text;
5663
+ if (typeof body !== "string" || !body || body.length > MAX_BODY) {
5664
+ throw new Error("invalid_reception_message_body");
5665
+ }
5666
+ if (envelope.payload?.transport !== "managed-e2ee") {
5667
+ throw new Error("reception_accepts_managed_messages_only");
5668
+ }
5669
+ const kind = envelope.payload?.messageKind === "rejection" ? "rejection" : "message";
5670
+ const sourceName = optionalText(envelope.payload?.sourceName, 100) ?? envelope.from.city;
5671
+ const inReplyTo = optionalText(envelope.payload?.inReplyTo, 180);
5672
+ return { envelope, body, kind, sourceName, inReplyTo, bytes: Buffer.byteLength(body, "utf8") };
5673
+ }
5674
+ function applyAutomaticRouting(database, messageIds) {
5675
+ if (!messageIds.length) return;
5676
+ const settings = database.prepare(
5677
+ `
5678
+ SELECT routing_mode, router_profile FROM reception_settings WHERE singleton = 1
5679
+ `
5680
+ ).get();
5681
+ if (settings?.routing_mode !== "auto" || settings.router_profile !== AUTO_ROUTER_PROFILE) return;
5682
+ const rawRules = database.prepare(
5683
+ `
5684
+ SELECT rule_id, target_city_id, target_city_address, keywords_json, priority
5685
+ FROM reception_auto_rules WHERE enabled = 1
5686
+ ORDER BY priority DESC, rule_id
5687
+ `
5688
+ ).all();
5689
+ const rules = rawRules.map(parseAutomaticRule).filter((rule) => Boolean(rule));
5690
+ if (!rules.length) return;
5691
+ const route = database.prepare(`
5692
+ INSERT INTO reception_routes (
5693
+ message_id, target_city_id, target_city_address, state, approved_by, approved_at
5694
+ ) VALUES (?, ?, ?, 'queued', 'auto', ?)
5695
+ `);
5696
+ const decide2 = database.prepare(`
5697
+ UPDATE reception_messages
5698
+ SET state = 'routed', decided_at = ?, decision_reason = ?
5699
+ WHERE message_id = ? AND state = 'pending' AND message_kind = 'message'
5700
+ `);
5701
+ for (const messageId of messageIds) {
5702
+ const message = database.prepare(
5703
+ `
5704
+ SELECT body, message_kind FROM reception_messages
5705
+ WHERE message_id = ? AND state = 'pending' LIMIT 1
5706
+ `
5707
+ ).get(messageId);
5708
+ if (message?.message_kind !== "message" || typeof message.body !== "string") continue;
5709
+ const selected = automaticDestination(message.body, rules);
5710
+ if (!selected) continue;
5711
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5712
+ route.run(messageId, selected.targetCityId, selected.targetCityAddress, now);
5713
+ const result2 = decide2.run(now, `auto:${selected.ruleId}`, messageId);
5714
+ if (Number(result2.changes) !== 1) throw new Error("automatic_reception_decision_conflict");
5715
+ }
5716
+ }
5717
+ function parseAutomaticRule(value) {
5718
+ let keywords;
5719
+ try {
5720
+ keywords = JSON.parse(String(value.keywords_json ?? ""));
5721
+ } catch {
5722
+ return null;
5723
+ }
5724
+ if (typeof value.rule_id !== "string" || typeof value.target_city_id !== "string" || typeof value.target_city_address !== "string" || !Array.isArray(keywords) || keywords.length < 1 || keywords.length > 20 || !keywords.every((keyword) => typeof keyword === "string" && keyword.length <= 80))
5725
+ return null;
5726
+ return {
5727
+ ruleId: value.rule_id,
5728
+ targetCityId: value.target_city_id,
5729
+ targetCityAddress: value.target_city_address,
5730
+ keywords: keywords.map((keyword) => normalizeForRouting(keyword)).filter(Boolean),
5731
+ priority: Number(value.priority) || 0
5732
+ };
5733
+ }
5734
+ function automaticDestination(body, rules) {
5735
+ const normalized = normalizeForRouting(body);
5736
+ if (!normalized || riskyAutomaticText(normalized)) return null;
5737
+ const matches = rules.map((rule) => ({
5738
+ rule,
5739
+ score: rule.keywords.reduce(
5740
+ (score, keyword) => score + (keyword === "*" || normalized.includes(keyword) ? 1 : 0),
5741
+ 0
5742
+ )
5743
+ })).filter((candidate) => candidate.score > 0);
5744
+ if (!matches.length) return null;
5745
+ matches.sort(
5746
+ (left, right) => right.score - left.score || right.rule.priority - left.rule.priority || left.rule.ruleId.localeCompare(right.rule.ruleId)
5747
+ );
5748
+ const winner = matches[0];
5749
+ const runnerUp = matches[1];
5750
+ if (runnerUp && runnerUp.score === winner.score && runnerUp.rule.priority === winner.rule.priority)
5751
+ return null;
5752
+ return winner.rule;
5753
+ }
5754
+ function normalizeForRouting(value) {
5755
+ return value.normalize("NFKC").toLocaleLowerCase("en").replace(/\s+/g, " ").trim();
5756
+ }
5757
+ function riskyAutomaticText(value) {
5758
+ return [
5759
+ /<\|(?:im_start|im_end|system|developer)\|>/i,
5760
+ /\[(?:system|inst|\/inst)\]/i,
5761
+ /\b(?:ignore|disregard|override)\b.{0,40}\b(?:previous|system|developer|instructions?)\b/i,
5762
+ /\b(?:system|developer)\s+(?:prompt|message)\b/i,
5763
+ /\b(?:reveal|print|send|exfiltrate)\b.{0,50}\b(?:password|secret|api[ -]?key|token|credential)\b/i,
5764
+ /\b(?:run|execute)\b.{0,30}\b(?:shell|command|terminal|tool)\b/i
5765
+ ].some((pattern) => pattern.test(value));
5766
+ }
5767
+ function syncReceptionConnections(appHome, connections2) {
5768
+ const database = receptionDatabase(appHome);
5769
+ let committed = false;
5770
+ try {
5771
+ database.exec("BEGIN IMMEDIATE");
5772
+ database.prepare("UPDATE reception_connections SET status = 'inactive'").run();
5773
+ const upsert = database.prepare(`
5774
+ INSERT INTO reception_connections (
5775
+ road_id, connection_id, peer_name, peer_endpoint, status, updated_at
5776
+ ) VALUES (?, ?, ?, ?, 'active', ?)
5777
+ ON CONFLICT(road_id) DO UPDATE SET
5778
+ connection_id = excluded.connection_id,
5779
+ peer_name = excluded.peer_name,
5780
+ peer_endpoint = excluded.peer_endpoint,
5781
+ status = 'active',
5782
+ updated_at = excluded.updated_at
5783
+ `);
5784
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5785
+ for (const connection of connections2) {
5786
+ if (!connection.roadId || !connection.connectionId || !connection.peerName.trim() || connection.peerName.length > 100 || !connection.peerEndpoint)
5787
+ throw new Error("invalid_reception_connection");
5788
+ upsert.run(
5789
+ connection.roadId,
5790
+ connection.connectionId,
5791
+ connection.peerName.trim(),
5792
+ connection.peerEndpoint,
5793
+ now
5794
+ );
5795
+ }
5796
+ database.exec("COMMIT");
5797
+ committed = true;
5798
+ } finally {
5799
+ if (!committed) {
5800
+ try {
5801
+ database.exec("ROLLBACK");
5802
+ } catch {
5803
+ }
5804
+ }
5805
+ }
5806
+ }
5807
+ function pendingReceptionOutbox(appHome, limit = 20) {
5808
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 20) {
5809
+ throw new Error("invalid_reception_outbox_batch");
5810
+ }
5811
+ return receptionDatabase(appHome).prepare(
5812
+ `
5813
+ SELECT o.message_id, o.road_id, o.connection_id, o.kind, o.body,
5814
+ o.in_reply_to, o.attempt_count
5815
+ FROM reception_outbox o
5816
+ JOIN reception_connections c ON c.road_id = o.road_id
5817
+ AND c.connection_id = o.connection_id
5818
+ WHERE o.state = 'queued' AND o.body IS NOT NULL AND c.status = 'active'
5819
+ AND (o.next_attempt_at IS NULL OR o.next_attempt_at <= ?)
5820
+ ORDER BY o.created_at, o.message_id
5821
+ LIMIT ?
5822
+ `
5823
+ ).all(Date.now(), limit).map((row) => {
5824
+ const value = row;
5825
+ return {
5826
+ messageId: String(value.message_id),
5827
+ roadId: String(value.road_id),
5828
+ connectionId: String(value.connection_id),
5829
+ kind: value.kind === "rejection" ? "rejection" : "message",
5830
+ body: String(value.body),
5831
+ inReplyTo: typeof value.in_reply_to === "string" ? value.in_reply_to : null,
5832
+ attemptCount: Number(value.attempt_count)
5833
+ };
5834
+ });
5835
+ }
5836
+ function markReceptionOutboxSent(appHome, messageId) {
5837
+ receptionDatabase(appHome).prepare(
5838
+ `
5839
+ UPDATE reception_outbox
5840
+ SET state = 'sent', body = NULL, sent_at = ?, next_attempt_at = NULL, error = NULL
5841
+ WHERE message_id = ? AND state = 'queued'
5842
+ `
5843
+ ).run((/* @__PURE__ */ new Date()).toISOString(), messageId);
5844
+ }
5845
+ function markReceptionOutboxFailed(appHome, messageId, attemptCount, error) {
5846
+ const attempts = Math.max(1, attemptCount + 1);
5847
+ const retryAt = Date.now() + Math.min(3e5, 1e3 * 2 ** Math.min(attempts - 1, 8));
5848
+ receptionDatabase(appHome).prepare(
5849
+ `
5850
+ UPDATE reception_outbox
5851
+ SET attempt_count = ?, last_attempt_at = ?, next_attempt_at = ?, error = ?
5852
+ WHERE message_id = ? AND state = 'queued'
5853
+ `
5854
+ ).run(
5855
+ attempts,
5856
+ Date.now(),
5857
+ retryAt,
5858
+ String(error instanceof Error ? error.message : error).slice(0, 300),
5859
+ messageId
5860
+ );
5861
+ }
5862
+ function deliverApprovedReception(context2, limit = DELIVERY_BATCH) {
5863
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > DELIVERY_BATCH) {
5864
+ throw new Error("invalid_reception_delivery_batch");
5865
+ }
5866
+ const database = receptionDatabase(context2.appHome);
5867
+ const routes = database.prepare(
5868
+ `
5869
+ SELECT m.message_id, m.source_city, m.source_created_at, m.body,
5870
+ m.connection_id, m.road_id, r.approved_at, r.approved_by,
5871
+ r.attempt_count
5872
+ FROM reception_routes r
5873
+ JOIN reception_messages m ON m.message_id = r.message_id
5874
+ WHERE r.target_city_id = ? AND r.target_city_address = ?
5875
+ AND r.state = 'queued' AND m.state = 'routed' AND m.body IS NOT NULL
5876
+ AND (r.next_attempt_at IS NULL OR r.next_attempt_at <= ?)
5877
+ ORDER BY r.approved_at, m.received_at, m.message_id
5878
+ LIMIT ?
5879
+ `
5880
+ ).all(context2.city.id, context2.city.address, Date.now(), limit);
5881
+ let delivered = 0;
5882
+ let failed = 0;
5883
+ for (const route of routes) {
5884
+ try {
5885
+ const envelope = {
5886
+ protocol: BUS_PROTOCOL,
5887
+ id: route.message_id,
5888
+ kind: "road.message",
5889
+ scope: "road",
5890
+ thread: null,
5891
+ from: { city: route.source_city, actor: "seat", role: "external-seat" },
5892
+ to: { city: context2.city.address, actor: "seat" },
5893
+ createdAt: route.source_created_at,
5894
+ payload: {
5895
+ text: wrapUntrusted(route.body, route.source_city).text,
5896
+ trust: "information-not-authority",
5897
+ transport: "reception-approved",
5898
+ reception: {
5899
+ approvedAt: route.approved_at,
5900
+ approvedBy: route.approved_by,
5901
+ sourceMessageId: route.message_id,
5902
+ ...route.connection_id ? { connectionId: route.connection_id } : {},
5903
+ ...route.road_id ? { roadId: route.road_id } : {}
5904
+ }
5905
+ }
5906
+ };
5907
+ const accepted = recordRoadInbox(context2.runtimeDir, envelope);
5908
+ if (accepted) markRoadInboxAccepted(context2.runtimeDir, envelope.id);
5909
+ markRouteDelivered(database, route.message_id, context2.city.id);
5910
+ delivered += 1;
5911
+ } catch (error) {
5912
+ if (error instanceof Error && error.message === "road_inbox_full") break;
5913
+ markRouteFailed(database, route.message_id, context2.city.id, error);
5914
+ failed += 1;
5915
+ }
5916
+ }
5917
+ const remaining = Number(
5918
+ database.prepare(
5919
+ `
5920
+ SELECT COUNT(*) AS count FROM reception_routes
5921
+ WHERE target_city_id = ? AND target_city_address = ? AND state = 'queued'
5922
+ `
5923
+ ).get(context2.city.id, context2.city.address)?.count ?? 0
5924
+ );
5925
+ return { delivered, failed, remaining };
5926
+ }
5927
+ function receptionDatabase(appHome) {
5928
+ const path = receptionDatabasePath(appHome);
5929
+ const cached = databases.get(path);
5930
+ if (cached) return cached;
5931
+ const directory = preparePrivateReceptionDirectory(appHome);
5932
+ if (existsSync6(path)) assertRegularPrivateDatabase(path);
5933
+ const database = new DatabaseSync(path);
5934
+ chmodSync3(path, 384);
5935
+ database.exec("PRAGMA journal_mode = WAL");
5936
+ database.exec("PRAGMA synchronous = FULL");
5937
+ database.exec("PRAGMA foreign_keys = ON");
5938
+ database.exec("PRAGMA busy_timeout = 5000");
5939
+ initializeSchema(database);
5940
+ chmodSync3(directory, 448);
5941
+ databases.set(path, database);
5942
+ return database;
5943
+ }
5944
+ function preparePrivateReceptionDirectory(appHome) {
5945
+ const home = realpathSync(resolve2(appHome));
5946
+ const runtime = join9(home, ".runtime");
5947
+ const reception = join9(runtime, "reception");
5948
+ for (const path of [runtime, reception]) {
5949
+ if (!existsSync6(path)) mkdirSync7(path, { mode: 448 });
5950
+ const info = lstatSync(path);
5951
+ if (!info.isDirectory() || info.isSymbolicLink()) {
5952
+ throw new Error(`unsafe_reception_directory:${path}`);
5953
+ }
5954
+ chmodSync3(path, 448);
5955
+ }
5956
+ return reception;
5957
+ }
5958
+ function assertRegularPrivateDatabase(path) {
5959
+ const info = lstatSync(path);
5960
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error("unsafe_reception_database");
5961
+ chmodSync3(path, 384);
5962
+ }
5963
+ function initializeSchema(database) {
5964
+ database.exec(`
5965
+ CREATE TABLE IF NOT EXISTS reception_meta (
5966
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
5967
+ schema_version INTEGER NOT NULL,
5968
+ created_at TEXT NOT NULL
5969
+ );
5970
+ CREATE TABLE IF NOT EXISTS reception_settings (
5971
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
5972
+ routing_mode TEXT NOT NULL DEFAULT 'manual' CHECK (routing_mode IN ('manual', 'auto')),
5973
+ review_policy TEXT NOT NULL DEFAULT 'every_message' CHECK (review_policy IN ('every_message', 'new_thread')),
5974
+ router_profile TEXT CHECK (router_profile IS NULL OR length(router_profile) <= 160),
5975
+ updated_at TEXT NOT NULL,
5976
+ CHECK (routing_mode = 'manual' OR router_profile IS NOT NULL)
5977
+ );
5978
+ CREATE TABLE IF NOT EXISTS reception_messages (
5979
+ message_id TEXT PRIMARY KEY CHECK (length(message_id) BETWEEN 1 AND 180),
5980
+ protocol TEXT NOT NULL CHECK (protocol = '${RECEPTION_PROTOCOL}'),
5981
+ state TEXT NOT NULL CHECK (state IN ('pending', 'routed', 'rejected', 'expired')),
5982
+ source_city TEXT NOT NULL CHECK (length(source_city) BETWEEN 3 AND 160),
5983
+ source_created_at TEXT NOT NULL,
5984
+ source_name TEXT CHECK (source_name IS NULL OR length(source_name) <= 100),
5985
+ message_kind TEXT NOT NULL DEFAULT 'message' CHECK (message_kind IN ('message', 'rejection')),
5986
+ in_reply_to TEXT CHECK (in_reply_to IS NULL OR length(in_reply_to) <= 180),
5987
+ received_city_id TEXT NOT NULL CHECK (length(received_city_id) BETWEEN 1 AND 160),
5988
+ received_city_address TEXT NOT NULL CHECK (length(received_city_address) BETWEEN 3 AND 160),
5989
+ body TEXT,
5990
+ body_sha256 TEXT NOT NULL CHECK (length(body_sha256) = 64),
5991
+ connection_id TEXT,
5992
+ road_id TEXT,
5993
+ remote_message_id TEXT,
5994
+ received_at TEXT NOT NULL,
5995
+ decided_at TEXT,
5996
+ decision_reason TEXT CHECK (decision_reason IS NULL OR length(decision_reason) <= 500),
5997
+ CHECK (state = 'pending' OR decided_at IS NOT NULL),
5998
+ CHECK (state <> 'pending' OR body IS NOT NULL)
5999
+ );
6000
+ CREATE INDEX IF NOT EXISTS idx_reception_messages_state_age
6001
+ ON reception_messages (state, received_at, message_id);
6002
+ CREATE TABLE IF NOT EXISTS reception_routes (
6003
+ message_id TEXT NOT NULL REFERENCES reception_messages(message_id) ON DELETE CASCADE,
6004
+ target_city_id TEXT NOT NULL CHECK (length(target_city_id) BETWEEN 1 AND 160),
6005
+ target_city_address TEXT NOT NULL CHECK (length(target_city_address) BETWEEN 3 AND 160),
6006
+ state TEXT NOT NULL DEFAULT 'queued' CHECK (state IN ('queued', 'delivered', 'failed')),
6007
+ approved_by TEXT NOT NULL CHECK (approved_by IN ('human', 'auto')),
6008
+ approved_at TEXT NOT NULL,
6009
+ delivered_at TEXT,
6010
+ error TEXT CHECK (error IS NULL OR length(error) <= 300),
6011
+ attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
6012
+ last_attempt_at INTEGER,
6013
+ next_attempt_at INTEGER,
6014
+ PRIMARY KEY (message_id, target_city_id)
6015
+ );
6016
+ CREATE INDEX IF NOT EXISTS idx_reception_routes_city_state
6017
+ ON reception_routes (target_city_id, state, approved_at);
6018
+ CREATE TABLE IF NOT EXISTS reception_counters (
6019
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
6020
+ pending_count INTEGER NOT NULL DEFAULT 0 CHECK (pending_count >= 0),
6021
+ pending_bytes INTEGER NOT NULL DEFAULT 0 CHECK (pending_bytes >= 0)
6022
+ );
6023
+ CREATE TABLE IF NOT EXISTS reception_connections (
6024
+ road_id TEXT PRIMARY KEY,
6025
+ connection_id TEXT NOT NULL UNIQUE,
6026
+ peer_name TEXT NOT NULL CHECK (length(peer_name) BETWEEN 1 AND 100),
6027
+ peer_endpoint TEXT NOT NULL CHECK (length(peer_endpoint) BETWEEN 3 AND 160),
6028
+ status TEXT NOT NULL CHECK (status IN ('active', 'inactive')),
6029
+ updated_at TEXT NOT NULL
6030
+ );
6031
+ CREATE TABLE IF NOT EXISTS reception_outbox (
6032
+ message_id TEXT PRIMARY KEY CHECK (length(message_id) = 36),
6033
+ road_id TEXT NOT NULL,
6034
+ connection_id TEXT NOT NULL,
6035
+ kind TEXT NOT NULL CHECK (kind IN ('message', 'rejection')),
6036
+ body TEXT,
6037
+ in_reply_to TEXT CHECK (in_reply_to IS NULL OR length(in_reply_to) <= 180),
6038
+ state TEXT NOT NULL DEFAULT 'queued' CHECK (state IN ('queued', 'sent')),
6039
+ created_at TEXT NOT NULL,
6040
+ sent_at TEXT,
6041
+ attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
6042
+ last_attempt_at INTEGER,
6043
+ next_attempt_at INTEGER,
6044
+ error TEXT CHECK (error IS NULL OR length(error) <= 300)
6045
+ );
6046
+ CREATE INDEX IF NOT EXISTS idx_reception_outbox_state_age
6047
+ ON reception_outbox (state, next_attempt_at, created_at);
6048
+ CREATE TABLE IF NOT EXISTS reception_auto_rules (
6049
+ rule_id TEXT PRIMARY KEY CHECK (length(rule_id) = 36),
6050
+ target_city_id TEXT NOT NULL UNIQUE CHECK (length(target_city_id) BETWEEN 1 AND 160),
6051
+ target_city_address TEXT NOT NULL CHECK (length(target_city_address) BETWEEN 3 AND 160),
6052
+ keywords_json TEXT NOT NULL CHECK (length(keywords_json) BETWEEN 5 AND 2000),
6053
+ priority INTEGER NOT NULL DEFAULT 0 CHECK (priority BETWEEN 0 AND 1000),
6054
+ enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
6055
+ updated_at TEXT NOT NULL
6056
+ );
6057
+ CREATE TRIGGER IF NOT EXISTS reception_message_count_after_insert
6058
+ AFTER INSERT ON reception_messages WHEN NEW.state = 'pending'
6059
+ BEGIN
6060
+ UPDATE reception_counters
6061
+ SET pending_count = pending_count + 1,
6062
+ pending_bytes = pending_bytes + length(CAST(NEW.body AS BLOB))
6063
+ WHERE singleton = 1;
6064
+ END;
6065
+ CREATE TRIGGER IF NOT EXISTS reception_message_count_after_decision
6066
+ AFTER UPDATE OF state ON reception_messages
6067
+ WHEN OLD.state = 'pending' AND NEW.state <> 'pending'
6068
+ BEGIN
6069
+ UPDATE reception_counters
6070
+ SET pending_count = MAX(0, pending_count - 1),
6071
+ pending_bytes = MAX(0, pending_bytes - length(CAST(OLD.body AS BLOB)))
6072
+ WHERE singleton = 1;
6073
+ END;
6074
+ CREATE TRIGGER IF NOT EXISTS reception_message_count_after_delete
6075
+ AFTER DELETE ON reception_messages WHEN OLD.state = 'pending'
6076
+ BEGIN
6077
+ UPDATE reception_counters
6078
+ SET pending_count = MAX(0, pending_count - 1),
6079
+ pending_bytes = MAX(0, pending_bytes - length(CAST(OLD.body AS BLOB)))
6080
+ WHERE singleton = 1;
6081
+ END;
6082
+ `);
6083
+ const meta = database.prepare("SELECT schema_version FROM reception_meta WHERE singleton = 1").get();
6084
+ if (meta?.schema_version === 1) {
6085
+ database.exec("BEGIN IMMEDIATE");
6086
+ try {
6087
+ const current = database.prepare("SELECT schema_version FROM reception_meta WHERE singleton = 1").get();
6088
+ if (current?.schema_version === 1) {
6089
+ database.exec(`
6090
+ ALTER TABLE reception_messages ADD COLUMN source_name TEXT
6091
+ CHECK (source_name IS NULL OR length(source_name) <= 100);
6092
+ ALTER TABLE reception_messages ADD COLUMN message_kind TEXT NOT NULL DEFAULT 'message'
6093
+ CHECK (message_kind IN ('message', 'rejection'));
6094
+ ALTER TABLE reception_messages ADD COLUMN in_reply_to TEXT
6095
+ CHECK (in_reply_to IS NULL OR length(in_reply_to) <= 180);
6096
+ UPDATE reception_meta SET schema_version = ${RECEPTION_SCHEMA_VERSION} WHERE singleton = 1;
6097
+ `);
6098
+ } else if (current?.schema_version === 2) {
6099
+ database.prepare("UPDATE reception_meta SET schema_version = ? WHERE singleton = 1").run(RECEPTION_SCHEMA_VERSION);
6100
+ } else if (current?.schema_version !== RECEPTION_SCHEMA_VERSION) {
6101
+ throw new Error(`unsupported_reception_schema:${current?.schema_version ?? "missing"}`);
6102
+ }
6103
+ database.exec("COMMIT");
6104
+ } catch (error) {
6105
+ try {
6106
+ database.exec("ROLLBACK");
6107
+ } catch {
6108
+ }
6109
+ throw error;
6110
+ }
6111
+ } else if (meta && meta.schema_version !== RECEPTION_SCHEMA_VERSION) {
6112
+ throw new Error(`unsupported_reception_schema:${meta.schema_version}`);
6113
+ }
6114
+ database.prepare(
6115
+ `
6116
+ INSERT OR IGNORE INTO reception_meta (singleton, schema_version, created_at)
6117
+ VALUES (1, ?, ?)
6118
+ `
6119
+ ).run(RECEPTION_SCHEMA_VERSION, (/* @__PURE__ */ new Date()).toISOString());
6120
+ database.prepare(
6121
+ `
6122
+ INSERT OR IGNORE INTO reception_settings (
6123
+ singleton, routing_mode, review_policy, router_profile, updated_at
6124
+ ) VALUES (1, 'manual', 'every_message', NULL, ?)
6125
+ `
6126
+ ).run((/* @__PURE__ */ new Date()).toISOString());
6127
+ database.prepare(
6128
+ `
6129
+ INSERT OR IGNORE INTO reception_counters (singleton, pending_count, pending_bytes)
6130
+ VALUES (1, 0, 0)
6131
+ `
6132
+ ).run();
6133
+ }
6134
+ function receptionCounters(database) {
6135
+ const row = database.prepare("SELECT pending_count, pending_bytes FROM reception_counters WHERE singleton = 1").get();
6136
+ return {
6137
+ pending: Number(row?.pending_count ?? 0),
6138
+ pendingBytes: Number(row?.pending_bytes ?? 0)
6139
+ };
6140
+ }
6141
+ function expireOldReception(database) {
6142
+ const retentionDays = boundedInteger(process.env.CITY_RECEPTION_PENDING_DAYS, 30, 1, 90);
6143
+ database.prepare(
6144
+ `
6145
+ UPDATE reception_messages
6146
+ SET state = 'expired', body = NULL, decided_at = ?, decision_reason = 'expired locally'
6147
+ WHERE state = 'pending' AND received_at < ?
6148
+ `
6149
+ ).run(
6150
+ (/* @__PURE__ */ new Date()).toISOString(),
6151
+ new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1e3).toISOString()
6152
+ );
6153
+ database.prepare(
6154
+ `
6155
+ DELETE FROM reception_messages
6156
+ WHERE state IN ('rejected', 'expired') AND decided_at < ?
6157
+ `
6158
+ ).run(new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3).toISOString());
6159
+ database.prepare(
6160
+ `
6161
+ DELETE FROM reception_messages
6162
+ WHERE state = 'routed' AND body IS NULL AND decided_at < ?
6163
+ `
6164
+ ).run(new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3).toISOString());
6165
+ }
6166
+ function markRouteDelivered(database, messageId, cityId) {
6167
+ let committed = false;
6168
+ try {
6169
+ database.exec("BEGIN IMMEDIATE");
6170
+ database.prepare(
6171
+ `
6172
+ UPDATE reception_routes
6173
+ SET state = 'delivered', delivered_at = ?, error = NULL,
6174
+ next_attempt_at = NULL
6175
+ WHERE message_id = ? AND target_city_id = ? AND state = 'queued'
6176
+ `
6177
+ ).run((/* @__PURE__ */ new Date()).toISOString(), messageId, cityId);
6178
+ const waiting = Number(
6179
+ database.prepare(
6180
+ `
6181
+ SELECT COUNT(*) AS count FROM reception_routes
6182
+ WHERE message_id = ? AND state <> 'delivered'
6183
+ `
6184
+ ).get(messageId)?.count ?? 0
6185
+ );
6186
+ if (!waiting) {
6187
+ database.prepare(
6188
+ `
6189
+ UPDATE reception_messages SET body = NULL
6190
+ WHERE message_id = ? AND state = 'routed'
6191
+ `
6192
+ ).run(messageId);
6193
+ }
6194
+ database.exec("COMMIT");
6195
+ committed = true;
6196
+ } finally {
6197
+ if (!committed) {
6198
+ try {
6199
+ database.exec("ROLLBACK");
6200
+ } catch {
6201
+ }
6202
+ }
6203
+ }
6204
+ }
6205
+ function markRouteFailed(database, messageId, cityId, error) {
6206
+ const reason = String(error instanceof Error ? error.message : error).slice(0, 300);
6207
+ const row = database.prepare(
6208
+ `
6209
+ SELECT attempt_count FROM reception_routes
6210
+ WHERE message_id = ? AND target_city_id = ? AND state = 'queued'
6211
+ `
6212
+ ).get(messageId, cityId);
6213
+ const attempts = Number(row?.attempt_count ?? 0) + 1;
6214
+ const now = Date.now();
6215
+ const retryAt = now + Math.min(3e5, 1e3 * 2 ** Math.min(attempts - 1, 8));
6216
+ database.prepare(
6217
+ `
6218
+ UPDATE reception_routes
6219
+ SET attempt_count = ?, last_attempt_at = ?, next_attempt_at = ?, error = ?
6220
+ WHERE message_id = ? AND target_city_id = ? AND state = 'queued'
6221
+ `
6222
+ ).run(attempts, now, retryAt, reason, messageId, cityId);
6223
+ }
6224
+ function optionalText(value, maximum) {
6225
+ return typeof value === "string" && value ? value.slice(0, maximum) : null;
6226
+ }
6227
+ function sha256(value) {
6228
+ return createHash("sha256").update(value, "utf8").digest("hex");
6229
+ }
6230
+ function boundedInteger(raw, fallback, minimum, maximum) {
6231
+ const value = Number(raw);
6232
+ return Number.isSafeInteger(value) && value >= minimum && value <= maximum ? value : fallback;
6233
+ }
6234
+
5410
6235
  // hub/local-roads.ts
5411
6236
  import { readFileSync as readFileSync7 } from "fs";
5412
- import { join as join9 } from "path";
6237
+ import { join as join10 } from "path";
5413
6238
  async function sendLocalRoad(context2, road, envelope) {
5414
6239
  const destinationRuntime = runtimeDirForCity(context2.appHome, road.id);
5415
6240
  let endpoint;
5416
6241
  try {
5417
6242
  endpoint = JSON.parse(
5418
- readFileSync7(join9(destinationRuntime, "endpoint.json"), "utf8")
6243
+ readFileSync7(join10(destinationRuntime, "endpoint.json"), "utf8")
5419
6244
  );
5420
6245
  if (endpoint.cityId !== road.id || endpoint.cityAddress !== road.address)
5421
6246
  throw new Error("identity mismatch");
@@ -5433,7 +6258,7 @@ async function sendLocalRoad(context2, road, envelope) {
5433
6258
  function localRoadOnline(context2, road) {
5434
6259
  try {
5435
6260
  const endpoint = JSON.parse(
5436
- readFileSync7(join9(runtimeDirForCity(context2.appHome, road.id), "endpoint.json"), "utf8")
6261
+ readFileSync7(join10(runtimeDirForCity(context2.appHome, road.id), "endpoint.json"), "utf8")
5437
6262
  );
5438
6263
  if (endpoint.cityId !== road.id || endpoint.cityAddress !== road.address || endpoint.pid <= 0)
5439
6264
  return false;
@@ -5444,7 +6269,7 @@ function localRoadOnline(context2, road) {
5444
6269
  }
5445
6270
  }
5446
6271
  function deliver(endpoint, from, envelope) {
5447
- return new Promise((resolve2, reject) => {
6272
+ return new Promise((resolve4, reject) => {
5448
6273
  const url = new URL(endpoint.url);
5449
6274
  url.searchParams.set("mode", "road");
5450
6275
  url.searchParams.set("from", from);
@@ -5463,7 +6288,7 @@ function deliver(endpoint, from, envelope) {
5463
6288
  } catch {
5464
6289
  }
5465
6290
  if (error) reject(error);
5466
- else resolve2(`delivered locally to ${endpoint.cityAddress}`);
6291
+ else resolve4(`delivered locally to ${endpoint.cityAddress}`);
5467
6292
  };
5468
6293
  ws.on("message", (raw) => {
5469
6294
  let message;
@@ -5487,8 +6312,692 @@ function deliver(endpoint, from, envelope) {
5487
6312
  });
5488
6313
  }
5489
6314
 
6315
+ // managed-connect/storage.ts
6316
+ import { createHash as createHash2 } from "node:crypto";
6317
+ import {
6318
+ chmodSync as chmodSync4,
6319
+ closeSync as closeSync3,
6320
+ constants as constants2,
6321
+ existsSync as existsSync7,
6322
+ fstatSync,
6323
+ fsyncSync as fsyncSync2,
6324
+ lstatSync as lstatSync2,
6325
+ mkdirSync as mkdirSync8,
6326
+ openSync as openSync3,
6327
+ readFileSync as readFileSync8,
6328
+ realpathSync as realpathSync2,
6329
+ renameSync as renameSync5,
6330
+ unlinkSync as unlinkSync4,
6331
+ writeFileSync as writeFileSync6
6332
+ } from "node:fs";
6333
+ import { homedir as homedir2 } from "node:os";
6334
+ import { dirname as dirname2, join as join11, resolve as resolve3 } from "node:path";
6335
+ import {
6336
+ KEY_TRANSPARENCY_ROOT_CHAIN_PROTOCOL,
6337
+ createOsProtectedDeviceVault,
6338
+ initializeHybridCrypto,
6339
+ parseKeyTransparencyRootChain,
6340
+ resolveKeyTransparencyRootChain
6341
+ } from "./managed-connect-client.js";
6342
+ var CONNECT_STATE_PROTOCOL = "agents-city-connect-state/3";
6343
+ var PLAINTEXT_KEY_CONNECT_STATE_PROTOCOL = "agents-city-connect-state/1";
6344
+ var UNVERSIONED_TRUST_CONNECT_STATE_PROTOCOL = "agents-city-connect-state/2";
6345
+ var MAX_STATE_BYTES = 128 * 1024;
6346
+ var MAX_ROOT_CHAIN_BYTES = 128 * 1024;
6347
+ var CITY_ADDRESS_RE = /^[a-z0-9][a-z0-9_-]{0,31}\/[a-z0-9][a-z0-9_-]{0,31}$/;
6348
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
6349
+ var OWNER_RE = /^[a-z0-9][a-z0-9_-]{0,31}$/;
6350
+ function agentsCityHome(explicit = "") {
6351
+ const requested = resolve3(
6352
+ explicit || process.env.AGENTS_CITY_HOME || join11(homedir2(), ".agents-city")
6353
+ );
6354
+ mkdirSync8(requested, { recursive: true, mode: 448 });
6355
+ return realpathSync2(requested);
6356
+ }
6357
+ function connectStateDirectory(appHome = "") {
6358
+ return join11(agentsCityHome(appHome), ".runtime", "connect");
6359
+ }
6360
+ function connectStatePath(appHome = "") {
6361
+ return join11(connectStateDirectory(appHome), "device.json");
6362
+ }
6363
+ function connectVaultDirectory(appHome = "") {
6364
+ return join11(connectStateDirectory(appHome), "vault");
6365
+ }
6366
+ var vaultAccount = (appHome = "") => {
6367
+ const digest = createHash2("sha256").update(agentsCityHome(appHome)).digest("hex");
6368
+ return `agents-city-connect-${digest}`;
6369
+ };
6370
+ function privateDirectory(path) {
6371
+ if (!existsSync7(path)) mkdirSync8(path, { mode: 448 });
6372
+ const info = lstatSync2(path);
6373
+ if (!info.isDirectory() || info.isSymbolicLink()) {
6374
+ throw new Error(`unsafe_connect_state_directory:${path}`);
6375
+ }
6376
+ chmodSync4(path, 448);
6377
+ }
6378
+ function prepareStateDirectory(appHome = "") {
6379
+ const home = agentsCityHome(appHome);
6380
+ const runtime = join11(home, ".runtime");
6381
+ privateDirectory(runtime);
6382
+ const connect = join11(runtime, "connect");
6383
+ privateDirectory(connect);
6384
+ return connect;
6385
+ }
6386
+ function assertSafeStateDirectory(appHome = "") {
6387
+ const home = agentsCityHome(appHome);
6388
+ const runtime = join11(home, ".runtime");
6389
+ const connect = join11(runtime, "connect");
6390
+ for (const path of [runtime, connect]) {
6391
+ const info = lstatSync2(path);
6392
+ if (!info.isDirectory() || info.isSymbolicLink()) {
6393
+ throw new Error(`unsafe_connect_state_directory:${path}`);
6394
+ }
6395
+ }
6396
+ if ((lstatSync2(connect).mode & 63) !== 0) {
6397
+ throw new Error("connect_state_directory_permissions_too_open");
6398
+ }
6399
+ return connect;
6400
+ }
6401
+ function assertPrivateFile(path) {
6402
+ const info = lstatSync2(path);
6403
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error("unsafe_connect_state_file");
6404
+ if ((info.mode & 63) !== 0) throw new Error("connect_state_permissions_too_open");
6405
+ if (info.size < 2 || info.size > MAX_STATE_BYTES) throw new Error("invalid_connect_state_size");
6406
+ }
6407
+ var noFollowFlag = () => typeof constants2.O_NOFOLLOW === "number" ? constants2.O_NOFOLLOW : 0;
6408
+ function writeConnectState(state, appHome = "") {
6409
+ const checked = validateConnectState(state);
6410
+ const directory = prepareStateDirectory(appHome);
6411
+ const destination = join11(directory, "device.json");
6412
+ const temporary = join11(directory, `.device-${process.pid}-${crypto.randomUUID()}.tmp`);
6413
+ const fd = openSync3(
6414
+ temporary,
6415
+ constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | noFollowFlag(),
6416
+ 384
6417
+ );
6418
+ try {
6419
+ writeFileSync6(fd, `${JSON.stringify(checked, null, 2)}
6420
+ `, { encoding: "utf8" });
6421
+ fsyncSync2(fd);
6422
+ } finally {
6423
+ closeSync3(fd);
6424
+ }
6425
+ renameSync5(temporary, destination);
6426
+ chmodSync4(destination, 384);
6427
+ try {
6428
+ const directoryFd = openSync3(directory, constants2.O_RDONLY);
6429
+ try {
6430
+ fsyncSync2(directoryFd);
6431
+ } finally {
6432
+ closeSync3(directoryFd);
6433
+ }
6434
+ } catch {
6435
+ }
6436
+ }
6437
+ function readConnectState(appHome = "") {
6438
+ const path = connectStatePath(appHome);
6439
+ if (!existsSync7(path)) return null;
6440
+ assertSafeStateDirectory(appHome);
6441
+ assertPrivateFile(path);
6442
+ const fd = openSync3(path, constants2.O_RDONLY | noFollowFlag());
6443
+ try {
6444
+ const info = fstatSync(fd);
6445
+ if (!info.isFile() || info.size > MAX_STATE_BYTES) {
6446
+ throw new Error("invalid_connect_state_size");
6447
+ }
6448
+ const value = JSON.parse(readFileSync8(fd, "utf8"));
6449
+ if (value.protocol === PLAINTEXT_KEY_CONNECT_STATE_PROTOCOL) {
6450
+ throw new Error("legacy_connect_state_contains_plaintext_keys");
6451
+ }
6452
+ if (value.protocol === UNVERSIONED_TRUST_CONNECT_STATE_PROTOCOL) {
6453
+ throw new Error("connect_state_requires_versioned_trust_repairing");
6454
+ }
6455
+ return validateConnectState(value);
6456
+ } catch (error) {
6457
+ if (error instanceof SyntaxError) throw new Error("invalid_connect_state_json");
6458
+ throw error;
6459
+ } finally {
6460
+ closeSync3(fd);
6461
+ }
6462
+ }
6463
+ function secureWebOrigin(value) {
6464
+ let url;
6465
+ try {
6466
+ url = new URL(String(value ?? ""));
6467
+ } catch {
6468
+ throw new Error("invalid_connect_service_url");
6469
+ }
6470
+ const local = ["127.0.0.1", "localhost", "[::1]", "::1"].includes(url.hostname);
6471
+ if (url.protocol !== "https:" && !(local && url.protocol === "http:")) {
6472
+ throw new Error("connect_service_requires_https");
6473
+ }
6474
+ if (url.username || url.password || url.search || url.hash) {
6475
+ throw new Error("invalid_connect_service_url");
6476
+ }
6477
+ if (url.pathname !== "/" && url.pathname !== "") {
6478
+ throw new Error("connect_service_must_be_an_origin");
6479
+ }
6480
+ url.pathname = "/";
6481
+ return url.toString().replace(/\/$/, "");
6482
+ }
6483
+ var normalizeConnectServiceUrl = (value) => secureWebOrigin(value);
6484
+ function transparencyResult(serviceUrl, root, trust) {
6485
+ const normalized = normalizeConnectServiceUrl(serviceUrl);
6486
+ if (root.signed.controlPlaneUrl !== normalized) {
6487
+ throw new Error("key_transparency_origin_mismatch");
6488
+ }
6489
+ return {
6490
+ stored: { root },
6491
+ runtime: { controlPlaneUrl: normalized, trust }
6492
+ };
6493
+ }
6494
+ async function resolveStoredTransparency(state) {
6495
+ const chain = {
6496
+ protocol: KEY_TRANSPARENCY_ROOT_CHAIN_PROTOCOL,
6497
+ roots: [state.keyTransparency.root]
6498
+ };
6499
+ const resolvedProfile = await resolveKeyTransparencyRootChain(chain, state.keyTransparency.root);
6500
+ const resolved = transparencyResult(
6501
+ state.serviceUrl,
6502
+ resolvedProfile.root,
6503
+ resolvedProfile.trust
6504
+ );
6505
+ if (state.status === "connected" && resolvedProfile.root.signed.relayUrl !== state.device.relayUrl)
6506
+ throw new Error("key_transparency_relay_mismatch");
6507
+ return resolved.runtime;
6508
+ }
6509
+ async function readRootChainResponse(response) {
6510
+ const declared = Number(response.headers.get("content-length") ?? 0);
6511
+ if (Number.isFinite(declared) && declared > MAX_ROOT_CHAIN_BYTES) {
6512
+ throw new Error("key_transparency_root_chain_too_large");
6513
+ }
6514
+ if (!response.body) throw new Error("empty_key_transparency_root_chain");
6515
+ const reader = response.body.getReader();
6516
+ const decoder = new TextDecoder();
6517
+ let total = 0;
6518
+ let body = "";
6519
+ while (true) {
6520
+ const { done, value } = await reader.read();
6521
+ if (done) break;
6522
+ total += value.byteLength;
6523
+ if (total > MAX_ROOT_CHAIN_BYTES) {
6524
+ await reader.cancel();
6525
+ throw new Error("key_transparency_root_chain_too_large");
6526
+ }
6527
+ body += decoder.decode(value, { stream: true });
6528
+ }
6529
+ body += decoder.decode();
6530
+ try {
6531
+ return parseKeyTransparencyRootChain(JSON.parse(body));
6532
+ } catch {
6533
+ throw new Error("invalid_key_transparency_root_chain_response");
6534
+ }
6535
+ }
6536
+ async function refreshStoredTransparency(appHome = "", fetcher = fetch) {
6537
+ const state = readConnectState(appHome);
6538
+ if (!state) throw new Error("connect_state_missing");
6539
+ let cached = null;
6540
+ let cachedError = null;
6541
+ try {
6542
+ cached = await resolveStoredTransparency(state);
6543
+ } catch (error) {
6544
+ cachedError = error;
6545
+ }
6546
+ const endpoint = new URL("/api/key-transparency/roots", state.serviceUrl);
6547
+ endpoint.searchParams.set("from", String(state.keyTransparency.root.signed.version));
6548
+ let response;
6549
+ try {
6550
+ response = await fetcher(endpoint, {
6551
+ headers: { accept: "application/json" },
6552
+ redirect: "error",
6553
+ signal: AbortSignal.timeout(5e3)
6554
+ });
6555
+ } catch {
6556
+ if (!cached) throw cachedError;
6557
+ return {
6558
+ state,
6559
+ runtime: cached,
6560
+ updated: false,
6561
+ refreshWarning: "key_transparency_root_refresh_unavailable"
6562
+ };
6563
+ }
6564
+ if (!response.ok) {
6565
+ if (!cached) throw cachedError;
6566
+ return {
6567
+ state,
6568
+ runtime: cached,
6569
+ updated: false,
6570
+ refreshWarning: `key_transparency_root_refresh_http_${response.status}`
6571
+ };
6572
+ }
6573
+ const chain = await readRootChainResponse(response);
6574
+ const latestState = readConnectState(appHome);
6575
+ if (!latestState) throw new Error("connect_state_missing");
6576
+ const resolvedProfile = await resolveKeyTransparencyRootChain(
6577
+ chain,
6578
+ latestState.keyTransparency.root
6579
+ );
6580
+ const resolved = transparencyResult(
6581
+ latestState.serviceUrl,
6582
+ resolvedProfile.root,
6583
+ resolvedProfile.trust
6584
+ );
6585
+ if (latestState.status === "connected" && resolvedProfile.root.signed.relayUrl !== latestState.device.relayUrl)
6586
+ throw new Error("key_transparency_relay_mismatch");
6587
+ const updated = resolvedProfile.root.signed.version > latestState.keyTransparency.root.signed.version;
6588
+ const nextState = updated ? {
6589
+ ...latestState,
6590
+ ...latestState.status === "connected" ? { updatedAt: (/* @__PURE__ */ new Date()).toISOString() } : {},
6591
+ keyTransparency: resolved.stored
6592
+ } : latestState;
6593
+ if (updated) writeConnectState(nextState, appHome);
6594
+ return {
6595
+ state: nextState,
6596
+ runtime: resolved.runtime,
6597
+ updated,
6598
+ refreshWarning: null
6599
+ };
6600
+ }
6601
+ function secureRelayUrl(value) {
6602
+ let url;
6603
+ try {
6604
+ url = new URL(String(value ?? ""));
6605
+ } catch {
6606
+ throw new Error("invalid_relay_url");
6607
+ }
6608
+ const local = ["127.0.0.1", "localhost", "[::1]", "::1"].includes(url.hostname);
6609
+ if (url.protocol !== "wss:" && !(local && url.protocol === "ws:")) {
6610
+ throw new Error("relay_requires_wss");
6611
+ }
6612
+ if (url.username || url.password || url.search || url.hash || url.pathname !== "/v1/connect")
6613
+ throw new Error("invalid_relay_url");
6614
+ return url.toString();
6615
+ }
6616
+ var decodedLength = (value) => {
6617
+ if (typeof value !== "string" || !/^[A-Za-z0-9_-]+$/.test(value)) return -1;
6618
+ try {
6619
+ return Buffer.from(value, "base64url").byteLength;
6620
+ } catch {
6621
+ return -1;
6622
+ }
6623
+ };
6624
+ function validateStoredTransparency(value, serviceUrl) {
6625
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6626
+ throw new Error("invalid_key_transparency_profile");
6627
+ }
6628
+ const profile = value;
6629
+ if (Object.keys(profile).length !== 1 || !Object.hasOwn(profile, "root")) {
6630
+ throw new Error("invalid_key_transparency_profile");
6631
+ }
6632
+ const parsed = parseKeyTransparencyRootChain({
6633
+ protocol: KEY_TRANSPARENCY_ROOT_CHAIN_PROTOCOL,
6634
+ roots: [profile.root]
6635
+ });
6636
+ const root = parsed.roots[0];
6637
+ if (root.signed.controlPlaneUrl !== serviceUrl) {
6638
+ throw new Error("key_transparency_origin_mismatch");
6639
+ }
6640
+ return { root };
6641
+ }
6642
+ function validateAuthorization(value, serviceUrl) {
6643
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6644
+ throw new Error("invalid_device_authorization");
6645
+ }
6646
+ const authorization = value;
6647
+ if (typeof authorization.device_code !== "string" || !authorization.device_code.startsWith("pasco_") || typeof authorization.user_code !== "string" || !/^PASCO-[A-Z0-9-]{8,20}$/.test(authorization.user_code) || !Number.isSafeInteger(authorization.expires_in) || authorization.expires_in < 30 || authorization.expires_in > 3600 || !Number.isSafeInteger(authorization.interval) || authorization.interval < 1 || authorization.interval > 60 || typeof authorization.signing_key_thumbprint !== "string" || decodedLength(authorization.signing_key_thumbprint) !== 32)
6648
+ throw new Error("invalid_device_authorization");
6649
+ const verification = new URL(authorization.verification_uri);
6650
+ if (verification.origin !== new URL(serviceUrl).origin) {
6651
+ throw new Error("verification_origin_mismatch");
6652
+ }
6653
+ return { ...authorization };
6654
+ }
6655
+ function validateAssignment(value) {
6656
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6657
+ throw new Error("invalid_device_assignment");
6658
+ }
6659
+ const assignment = value;
6660
+ if (!UUID_RE.test(String(assignment.deviceId ?? "")) || !OWNER_RE.test(String(assignment.ownerPrefix ?? "")) || !Number.isSafeInteger(assignment.keyVersion) || assignment.keyVersion < 1)
6661
+ throw new Error("invalid_device_assignment");
6662
+ return {
6663
+ deviceId: assignment.deviceId,
6664
+ ownerPrefix: assignment.ownerPrefix,
6665
+ relayUrl: secureRelayUrl(assignment.relayUrl),
6666
+ keyVersion: assignment.keyVersion
6667
+ };
6668
+ }
6669
+ function validateBinding(value) {
6670
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6671
+ throw new Error("invalid_connected_city");
6672
+ }
6673
+ const city = value;
6674
+ const rawDataDir = String(city.dataDir ?? "");
6675
+ const dataDir2 = resolve3(rawDataDir);
6676
+ if (typeof city.localCityId !== "string" || !/^[A-Za-z0-9_-]{4,160}$/.test(city.localCityId) || !OWNER_RE.test(String(city.slug ?? "")) || typeof city.name !== "string" || !city.name.trim() || city.name.length > 100 || !CITY_ADDRESS_RE.test(String(city.remoteAddress ?? "")) || decodedLength(city.encryptionKeyId) !== 32 || typeof city.connected !== "boolean" || !rawDataDir.startsWith("/") || !dataDir2.startsWith("/"))
6677
+ throw new Error("invalid_connected_city");
6678
+ return { ...city, dataDir: dataDir2 };
6679
+ }
6680
+ function validateConnectState(value) {
6681
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6682
+ throw new Error("invalid_connect_state");
6683
+ }
6684
+ const state = value;
6685
+ if (state.protocol !== CONNECT_STATE_PROTOCOL) throw new Error("invalid_connect_state_protocol");
6686
+ const serviceUrl = normalizeConnectServiceUrl(state.serviceUrl);
6687
+ const keyTransparency = validateStoredTransparency(state.keyTransparency, serviceUrl);
6688
+ if (state.status === "pending") {
6689
+ if (typeof state.machineName !== "string" || !state.machineName.trim() || state.machineName.length > 100 || typeof state.createdAt !== "string" || !Number.isFinite(Date.parse(state.createdAt)))
6690
+ throw new Error("invalid_connect_state");
6691
+ return {
6692
+ protocol: CONNECT_STATE_PROTOCOL,
6693
+ status: "pending",
6694
+ serviceUrl,
6695
+ machineName: state.machineName,
6696
+ createdAt: state.createdAt,
6697
+ authorization: validateAuthorization(state.authorization, serviceUrl),
6698
+ keyTransparency
6699
+ };
6700
+ }
6701
+ if (state.status !== "connected" || typeof state.connectedAt !== "string" || !Number.isFinite(Date.parse(state.connectedAt)) || typeof state.updatedAt !== "string" || !Number.isFinite(Date.parse(state.updatedAt)) || !Array.isArray(state.cities) || state.cities.length > 100)
6702
+ throw new Error("invalid_connect_state");
6703
+ const cities = state.cities.map(validateBinding);
6704
+ if (new Set(cities.map((city) => city.localCityId)).size !== cities.length || new Set(cities.map((city) => city.remoteAddress)).size !== cities.length)
6705
+ throw new Error("duplicate_connected_city");
6706
+ const device = validateAssignment(state.device);
6707
+ if (keyTransparency.root.signed.relayUrl !== device.relayUrl) {
6708
+ throw new Error("key_transparency_relay_mismatch");
6709
+ }
6710
+ return {
6711
+ protocol: CONNECT_STATE_PROTOCOL,
6712
+ status: "connected",
6713
+ serviceUrl,
6714
+ connectedAt: state.connectedAt,
6715
+ updatedAt: state.updatedAt,
6716
+ device,
6717
+ keyTransparency,
6718
+ cities
6719
+ };
6720
+ }
6721
+ async function openConnectDeviceVault(appHome = "") {
6722
+ prepareStateDirectory(appHome);
6723
+ await initializeHybridCrypto();
6724
+ return createOsProtectedDeviceVault({
6725
+ directory: connectVaultDirectory(appHome),
6726
+ service: "agents-city-private-device",
6727
+ account: vaultAccount(appHome)
6728
+ });
6729
+ }
6730
+ async function loadConnectIdentity(state, appHome = "") {
6731
+ const vault = await openConnectDeviceVault(appHome);
6732
+ const identity = await vault.loadIdentity();
6733
+ if (!identity) throw new Error("connect_device_identity_missing");
6734
+ if (identity.deviceId !== state.device.deviceId || identity.ownerPrefix !== state.device.ownerPrefix || identity.relayUrl !== state.device.relayUrl || identity.keyVersion !== state.device.keyVersion)
6735
+ throw new Error("connect_device_identity_mismatch");
6736
+ return identity;
6737
+ }
6738
+ function connectedStateForCity(localCityId, appHome = "") {
6739
+ const state = readConnectState(appHome);
6740
+ if (!state || state.status !== "connected") return null;
6741
+ const binding = state.cities.find((city) => city.localCityId === localCityId && city.connected);
6742
+ return binding ? { state, binding } : null;
6743
+ }
6744
+
6745
+ // managed-connect/transport.ts
6746
+ import {
6747
+ ManagedRelaySession,
6748
+ initializeHybridCrypto as initializeHybridCrypto2,
6749
+ signedRelayHeaders
6750
+ } from "./managed-connect-client.js";
6751
+ var CITY_ADDRESS_RE2 = /^[a-z0-9][a-z0-9_-]{0,31}\/[a-z0-9][a-z0-9_-]{0,31}$/;
6752
+ var MAX_SERVER_FRAME_BYTES = 262144;
6753
+ async function openManagedRelaySession(identity, city, options) {
6754
+ if (!CITY_ADDRESS_RE2.test(city)) throw new Error("invalid_city_address");
6755
+ if (!options.keyTransparency) throw new Error("key_transparency_required");
6756
+ await initializeHybridCrypto2();
6757
+ const headers = await signedRelayHeaders(identity, city);
6758
+ const url = new URL(identity.relayUrl);
6759
+ const local = ["127.0.0.1", "localhost", "[::1]", "::1"].includes(url.hostname);
6760
+ if (url.protocol !== "wss:" && !(local && url.protocol === "ws:") || url.pathname !== "/v1/connect" || url.username || url.password || url.search || url.hash)
6761
+ throw new Error("invalid_relay_url");
6762
+ url.searchParams.set("city", city);
6763
+ const socket = new wrapper_default(url, {
6764
+ headers,
6765
+ handshakeTimeout: 1e4,
6766
+ maxPayload: MAX_SERVER_FRAME_BYTES,
6767
+ perMessageDeflate: false,
6768
+ followRedirects: false
6769
+ });
6770
+ socket.on("error", () => {
6771
+ });
6772
+ const transport = {
6773
+ send: (raw) => {
6774
+ if (socket.readyState !== wrapper_default.OPEN) throw new Error("relay_connection_closed");
6775
+ socket.send(raw);
6776
+ },
6777
+ close: (code, reason) => socket.close(code, reason),
6778
+ onMessage: (handler) => {
6779
+ socket.on("message", (raw, isBinary) => {
6780
+ handler(isBinary ? "" : String(raw));
6781
+ });
6782
+ },
6783
+ onClose: (handler) => {
6784
+ socket.on("close", handler);
6785
+ }
6786
+ };
6787
+ const session = new ManagedRelaySession(identity, city, transport, {
6788
+ ...options,
6789
+ sealedSender: options.sealedSender ?? {}
6790
+ });
6791
+ try {
6792
+ await new Promise((resolve4, reject) => {
6793
+ const timer = setTimeout(() => reject(new Error("relay_connection_timeout")), 1e4);
6794
+ socket.once("open", () => {
6795
+ clearTimeout(timer);
6796
+ resolve4();
6797
+ });
6798
+ socket.once("error", () => {
6799
+ clearTimeout(timer);
6800
+ reject(new Error("relay_connection_failed"));
6801
+ });
6802
+ });
6803
+ await session.ready();
6804
+ return { session, socket };
6805
+ } catch (error) {
6806
+ try {
6807
+ socket.close(1e3, "connection failed");
6808
+ } catch {
6809
+ }
6810
+ throw error;
6811
+ }
6812
+ }
6813
+
6814
+ // managed-connect/bridge.ts
6815
+ var INITIAL_BACKOFF_MS = 1e3;
6816
+ var MAX_BACKOFF_MS = 3e4;
6817
+ function managedRoadBridge(context2, receive, receiveManaged) {
6818
+ let session = null;
6819
+ let socket = null;
6820
+ let stopped = false;
6821
+ let connecting = false;
6822
+ let timer = null;
6823
+ let heartbeat = null;
6824
+ let backoff = INITIAL_BACKOFF_MS;
6825
+ let cachedRoads = [];
6826
+ let configured = false;
6827
+ let warnedState = false;
6828
+ const stateForCity = () => {
6829
+ try {
6830
+ const found = connectedStateForCity(context2.city.id, context2.appHome);
6831
+ configured = Boolean(found);
6832
+ return found;
6833
+ } catch (error) {
6834
+ configured = false;
6835
+ if (!warnedState) {
6836
+ warnedState = true;
6837
+ console.error(`[city-bus] managed Connect unavailable: ${error.message}`);
6838
+ }
6839
+ return null;
6840
+ }
6841
+ };
6842
+ const updateCache = () => {
6843
+ if (session) {
6844
+ cachedRoads = session.roads().map((road) => ({
6845
+ id: road.id,
6846
+ name: road.peerCity.split("/")[1] || road.peerCity,
6847
+ owner: road.peerCity.split("/")[0] || "remote",
6848
+ address: road.peerCity,
6849
+ local: false,
6850
+ managed: true,
6851
+ revision: road.revision
6852
+ }));
6853
+ }
6854
+ return cachedRoads.map((road) => ({ ...road }));
6855
+ };
6856
+ const schedule = () => {
6857
+ if (stopped || timer) return;
6858
+ timer = setTimeout(() => {
6859
+ timer = null;
6860
+ void connect();
6861
+ }, backoff);
6862
+ timer.unref();
6863
+ backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
6864
+ };
6865
+ const disconnected = () => {
6866
+ if (heartbeat) clearInterval(heartbeat);
6867
+ heartbeat = null;
6868
+ session = null;
6869
+ socket = null;
6870
+ connecting = false;
6871
+ if (!stopped) schedule();
6872
+ };
6873
+ const connect = async () => {
6874
+ if (stopped || connecting || session) return;
6875
+ const found = stateForCity();
6876
+ if (!found) {
6877
+ schedule();
6878
+ return;
6879
+ }
6880
+ connecting = true;
6881
+ try {
6882
+ const refreshed = await refreshStoredTransparency(context2.appHome);
6883
+ if (refreshed.state.status !== "connected") throw new Error("connect_state_not_connected");
6884
+ if (refreshed.refreshWarning && process.env.CITY_BUS_DEBUG === "1") {
6885
+ console.error(`[city-bus] ${refreshed.refreshWarning}; using unexpired cached root`);
6886
+ }
6887
+ const opened = await openManagedRelaySession(
6888
+ await loadConnectIdentity(refreshed.state, context2.appHome),
6889
+ found.binding.remoteAddress,
6890
+ {
6891
+ onText: async (message) => {
6892
+ const envelope = managedEnvelope(context2, message);
6893
+ if (!receiveManaged) throw new Error("managed_reception_receipt_unavailable");
6894
+ const result2 = receiveManaged(envelope);
6895
+ return {
6896
+ messageId: message.messageId,
6897
+ status: result2.inserted ? "inserted" : "duplicate"
6898
+ };
6899
+ },
6900
+ keyTransparency: refreshed.runtime,
6901
+ onSecurityError: (error) => {
6902
+ console.error(`[city-bus] managed Road frame rejected: ${error.message}`);
6903
+ },
6904
+ onLocalError: (error) => {
6905
+ console.error(`[city-bus] managed Road local handoff failed: ${error.message}`);
6906
+ }
6907
+ }
6908
+ );
6909
+ if (stopped) {
6910
+ opened.session.close();
6911
+ return;
6912
+ }
6913
+ session = opened.session;
6914
+ socket = opened.socket;
6915
+ updateCache();
6916
+ backoff = INITIAL_BACKOFF_MS;
6917
+ warnedState = false;
6918
+ connecting = false;
6919
+ opened.socket.once("close", disconnected);
6920
+ heartbeat = setInterval(() => {
6921
+ try {
6922
+ session?.ping();
6923
+ } catch {
6924
+ }
6925
+ }, 3e4);
6926
+ heartbeat.unref();
6927
+ } catch (error) {
6928
+ connecting = false;
6929
+ if (process.env.CITY_BUS_DEBUG === "1") {
6930
+ console.error(`[city-bus] managed Connect retry: ${error.message}`);
6931
+ }
6932
+ schedule();
6933
+ }
6934
+ };
6935
+ const send = async (to, envelope) => {
6936
+ const active = session;
6937
+ if (!active) throw new Error("the managed Road is not connected");
6938
+ const matches = active.roads().filter((road) => road.peerCity === to);
6939
+ if (matches.length !== 1) {
6940
+ throw new Error(
6941
+ matches.length ? "multiple managed Roads share that address" : "managed Road not available"
6942
+ );
6943
+ }
6944
+ const body = envelope.payload?.text;
6945
+ if (typeof body !== "string") throw new Error("managed Roads carry text only");
6946
+ const result2 = await active.sendRoadText(matches[0].id, body);
6947
+ if (result2.status === "duplicate") return `duplicate already accepted by ${to}`;
6948
+ return `encrypted message durably queued for ${to}`;
6949
+ };
6950
+ const close2 = () => {
6951
+ stopped = true;
6952
+ if (timer) clearTimeout(timer);
6953
+ if (heartbeat) clearInterval(heartbeat);
6954
+ timer = null;
6955
+ heartbeat = null;
6956
+ try {
6957
+ session?.close();
6958
+ } catch {
6959
+ }
6960
+ try {
6961
+ socket?.close();
6962
+ } catch {
6963
+ }
6964
+ session = null;
6965
+ socket = null;
6966
+ };
6967
+ return {
6968
+ start: () => {
6969
+ void connect();
6970
+ },
6971
+ close: close2,
6972
+ send,
6973
+ roads: updateCache,
6974
+ hasRoad: (address) => updateCache().some((road) => road.address === address),
6975
+ enabled: () => configured || Boolean(stateForCity()),
6976
+ online: (address) => Boolean(session) && updateCache().some((road) => road.address === address)
6977
+ };
6978
+ }
6979
+ function managedEnvelope(context2, message) {
6980
+ return {
6981
+ protocol: BUS_PROTOCOL,
6982
+ id: `managed_${message.messageId.replaceAll("-", "")}`,
6983
+ kind: "road.message",
6984
+ scope: "road",
6985
+ thread: null,
6986
+ from: { city: message.from, actor: "seat", role: "external-seat" },
6987
+ to: { city: context2.city.address, actor: "seat" },
6988
+ createdAt: isoNow(),
6989
+ payload: {
6990
+ text: message.text,
6991
+ trust: "information-not-authority",
6992
+ transport: "managed-e2ee",
6993
+ remoteMessageId: message.messageId,
6994
+ roadId: message.roadId
6995
+ }
6996
+ };
6997
+ }
6998
+
5490
6999
  // hub/remote-roads.ts
5491
- function remoteRoadBridge(context2, receive) {
7000
+ function legacyRemoteRoadBridge(context2, receive) {
5492
7001
  const base = process.env.CITY_BUS_URL || "";
5493
7002
  const token = process.env.CITY_BUS_TOKEN || "";
5494
7003
  const enabled = Boolean(base && token);
@@ -5558,7 +7067,7 @@ function remoteRoadBridge(context2, receive) {
5558
7067
  }
5559
7068
  if (envelope.scope === "road" && envelope.to.city === context2.city.address) receive(envelope);
5560
7069
  };
5561
- const sendRaw = (to, envelope) => new Promise((resolve2, reject) => {
7070
+ const sendRaw = (to, envelope) => new Promise((resolve4, reject) => {
5562
7071
  if (!enabled) return reject(new Error("no remote road transport is configured"));
5563
7072
  if (!online || !ws || ws.readyState !== wrapper_default.OPEN) {
5564
7073
  return reject(new Error("the remote road is not connected"));
@@ -5576,8 +7085,8 @@ function remoteRoadBridge(context2, receive) {
5576
7085
  cleanup();
5577
7086
  if (message.type === "error")
5578
7087
  reject(new Error(String(message.error || "remote road refused the message")));
5579
- else if (message.type === "queued") resolve2(`${to} is offline: queued remotely`);
5580
- else resolve2(`delivered remotely to ${to}`);
7088
+ else if (message.type === "queued") resolve4(`${to} is offline: queued remotely`);
7089
+ else resolve4(`delivered remotely to ${to}`);
5581
7090
  };
5582
7091
  const cleanup = () => {
5583
7092
  clearTimeout(timer);
@@ -5621,6 +7130,30 @@ function remoteRoadBridge(context2, receive) {
5621
7130
  online: (address) => roster.has(address)
5622
7131
  };
5623
7132
  }
7133
+ function remoteRoadBridge(context2, receive, receiveManaged) {
7134
+ const legacy = legacyRemoteRoadBridge(context2, (envelope) => {
7135
+ try {
7136
+ receive(envelope);
7137
+ } catch (error) {
7138
+ console.error(`[city-bus] dropped remote envelope: ${error.message}`);
7139
+ }
7140
+ });
7141
+ const managed = managedRoadBridge(context2, receive, receiveManaged);
7142
+ return {
7143
+ start: () => {
7144
+ legacy.start();
7145
+ managed.start();
7146
+ },
7147
+ close: () => {
7148
+ legacy.close();
7149
+ managed.close();
7150
+ },
7151
+ send: (to, envelope) => managed.hasRoad(to) ? managed.send(to, envelope) : legacy.send(to, envelope),
7152
+ enabled: () => legacy.enabled() || managed.enabled(),
7153
+ online: (address) => managed.online(address) || legacy.online(address),
7154
+ roads: managed.roads
7155
+ };
7156
+ }
5624
7157
  function legacyEnvelope(context2, from, body, id) {
5625
7158
  return {
5626
7159
  protocol: BUS_PROTOCOL,
@@ -5635,14 +7168,413 @@ function legacyEnvelope(context2, from, body, id) {
5635
7168
  };
5636
7169
  }
5637
7170
 
7171
+ // managed-connect/reception-bridge.ts
7172
+ import {
7173
+ closeSync as closeSync4,
7174
+ existsSync as existsSync8,
7175
+ lstatSync as lstatSync3,
7176
+ openSync as openSync4,
7177
+ readFileSync as readFileSync9,
7178
+ statSync as statSync4,
7179
+ unlinkSync as unlinkSync5,
7180
+ writeFileSync as writeFileSync7
7181
+ } from "node:fs";
7182
+ import { join as join12 } from "node:path";
7183
+
7184
+ // managed-connect/device.ts
7185
+ import {
7186
+ ConnectApiError,
7187
+ beginDeviceAuthorization,
7188
+ listDeviceRoads,
7189
+ pollDeviceAuthorization,
7190
+ signedRelayHeaders as signedRelayHeaders2,
7191
+ syncDeviceCities
7192
+ } from "./managed-connect-client.js";
7193
+
7194
+ // managed-connect/person-message.ts
7195
+ var PERSON_MESSAGE_PROTOCOL = "agents-city-person-message/1";
7196
+ var MAX_PERSON_TEXT_BYTES = 11500;
7197
+ var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7198
+ var textEncoder = new TextEncoder();
7199
+ var textDecoder = new TextDecoder("utf-8", { fatal: true });
7200
+ var utf8Length = (value) => textEncoder.encode(value).byteLength;
7201
+ function encodePersonMessage(message) {
7202
+ validateText(message.text);
7203
+ if (!["message", "rejection"].includes(message.kind)) {
7204
+ throw new Error("invalid_person_message_kind");
7205
+ }
7206
+ if (message.inReplyTo !== null && !UUID_RE2.test(message.inReplyTo)) {
7207
+ throw new Error("invalid_person_reply_reference");
7208
+ }
7209
+ return textDecoder.decode(
7210
+ textEncoder.encode(
7211
+ JSON.stringify({
7212
+ protocol: PERSON_MESSAGE_PROTOCOL,
7213
+ kind: message.kind,
7214
+ text: message.text,
7215
+ ...message.inReplyTo ? { inReplyTo: message.inReplyTo } : {}
7216
+ })
7217
+ )
7218
+ );
7219
+ }
7220
+ function decodePersonMessage(value) {
7221
+ validateText(value);
7222
+ let parsed;
7223
+ try {
7224
+ parsed = JSON.parse(value);
7225
+ } catch {
7226
+ return { kind: "message", text: value, inReplyTo: null };
7227
+ }
7228
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
7229
+ return { kind: "message", text: value, inReplyTo: null };
7230
+ }
7231
+ const record2 = parsed;
7232
+ const allowed = /* @__PURE__ */ new Set(["protocol", "kind", "text", "inReplyTo"]);
7233
+ if (record2.protocol !== PERSON_MESSAGE_PROTOCOL || !["message", "rejection"].includes(String(record2.kind)) || typeof record2.text !== "string" || !record2.text.trim() || utf8Length(record2.text) > MAX_PERSON_TEXT_BYTES || record2.inReplyTo !== void 0 && (typeof record2.inReplyTo !== "string" || !UUID_RE2.test(record2.inReplyTo)) || Object.keys(record2).some((key) => !allowed.has(key))) {
7234
+ return { kind: "message", text: value, inReplyTo: null };
7235
+ }
7236
+ return {
7237
+ kind: record2.kind,
7238
+ text: record2.text,
7239
+ inReplyTo: typeof record2.inReplyTo === "string" ? record2.inReplyTo : null
7240
+ };
7241
+ }
7242
+ function validateText(value) {
7243
+ if (typeof value !== "string" || !value.trim()) throw new Error("person_message_required");
7244
+ if (utf8Length(value) > MAX_PERSON_TEXT_BYTES) throw new Error("person_message_too_large");
7245
+ }
7246
+
7247
+ // managed-connect/reception-bridge.ts
7248
+ var RETRY_START_MS = 1e3;
7249
+ var RETRY_MAX_MS = 3e4;
7250
+ var DIRECTORY_REFRESH_MS = 3e4;
7251
+ var OUTBOX_INTERVAL_MS = 250;
7252
+ function managedReceptionBridge(context2) {
7253
+ let releaseLease = null;
7254
+ let session = null;
7255
+ let socket = null;
7256
+ let stopped = false;
7257
+ let connecting = false;
7258
+ let retryTimer = null;
7259
+ let refreshTimer = null;
7260
+ let outboxTimer = null;
7261
+ let heartbeat = null;
7262
+ let backoff = RETRY_START_MS;
7263
+ let draining = false;
7264
+ let metadata2 = /* @__PURE__ */ new Map();
7265
+ let identity = null;
7266
+ const debug2 = (message) => {
7267
+ if (process.env.CITY_BUS_DEBUG === "1") console.error(`[reception] ${message}`);
7268
+ };
7269
+ const schedule = (delay = backoff) => {
7270
+ if (stopped || retryTimer) return;
7271
+ retryTimer = setTimeout(() => {
7272
+ retryTimer = null;
7273
+ void connect();
7274
+ }, delay);
7275
+ retryTimer.unref();
7276
+ backoff = Math.min(RETRY_MAX_MS, Math.max(RETRY_START_MS, backoff * 2));
7277
+ };
7278
+ const clearConnection = () => {
7279
+ if (heartbeat) clearInterval(heartbeat);
7280
+ heartbeat = null;
7281
+ session = null;
7282
+ socket = null;
7283
+ connecting = false;
7284
+ };
7285
+ const identityFor = async (state) => {
7286
+ if (identity?.deviceId === state.device.deviceId) return identity;
7287
+ identity = await loadConnectIdentity(state, context2.appHome);
7288
+ return identity;
7289
+ };
7290
+ const refreshDirectory = async () => {
7291
+ const state = readConnectState(context2.appHome);
7292
+ if (!state || state.status !== "connected") return [];
7293
+ const activeIdentity = await identityFor(state);
7294
+ const endpoint = receptionEndpoint(activeIdentity.ownerPrefix, activeIdentity.deviceId);
7295
+ const directory = await listDeviceRoads(state.serviceUrl, activeIdentity);
7296
+ const roads2 = directory.roads.filter(
7297
+ (road) => road.kind === "connection" && Boolean(road.connectionId) && road.localCity === endpoint
7298
+ );
7299
+ metadata2 = new Map(roads2.map((road) => [road.id, road]));
7300
+ syncReceptionConnections(
7301
+ context2.appHome,
7302
+ roads2.map((road) => ({
7303
+ roadId: road.id,
7304
+ connectionId: road.connectionId,
7305
+ peerName: road.peerName,
7306
+ peerEndpoint: road.peerCity
7307
+ }))
7308
+ );
7309
+ return roads2;
7310
+ };
7311
+ const connect = async () => {
7312
+ if (stopped || connecting || session) return;
7313
+ const state = readConnectState(context2.appHome);
7314
+ if (!state || state.status !== "connected") return schedule(DIRECTORY_REFRESH_MS);
7315
+ if (!releaseLease) {
7316
+ releaseLease = tryAcquireReceptionLease(context2.appHome);
7317
+ if (!releaseLease) return schedule(5e3);
7318
+ }
7319
+ connecting = true;
7320
+ try {
7321
+ const refreshed = await refreshStoredTransparency(context2.appHome);
7322
+ if (refreshed.state.status !== "connected") throw new Error("connect_state_not_connected");
7323
+ if (refreshed.refreshWarning) {
7324
+ debug2(`${refreshed.refreshWarning}; using unexpired cached root`);
7325
+ }
7326
+ const activeIdentity = await identityFor(refreshed.state);
7327
+ const roads2 = await refreshDirectory();
7328
+ if (!roads2.length) {
7329
+ connecting = false;
7330
+ backoff = RETRY_START_MS;
7331
+ return schedule(DIRECTORY_REFRESH_MS);
7332
+ }
7333
+ const endpoint = receptionEndpoint(activeIdentity.ownerPrefix, activeIdentity.deviceId);
7334
+ const opened = await openManagedRelaySession(activeIdentity, endpoint, {
7335
+ onText: async (message) => {
7336
+ const recorded = recordReceptionMessage(
7337
+ {
7338
+ appHome: context2.appHome,
7339
+ city: { id: `device_${activeIdentity.deviceId}`, address: endpoint }
7340
+ },
7341
+ receptionEnvelope(
7342
+ activeIdentity.deviceId,
7343
+ endpoint,
7344
+ message,
7345
+ metadata2.get(message.roadId)
7346
+ )
7347
+ );
7348
+ return {
7349
+ messageId: message.messageId,
7350
+ status: recorded.inserted ? "inserted" : "duplicate"
7351
+ };
7352
+ },
7353
+ keyTransparency: refreshed.runtime,
7354
+ onSecurityError: (error) => debug2(`security refusal: ${error.message}`),
7355
+ onLocalError: (error) => debug2(`local handoff failed: ${error.message}`)
7356
+ });
7357
+ if (stopped) {
7358
+ opened.session.close();
7359
+ return;
7360
+ }
7361
+ session = opened.session;
7362
+ socket = opened.socket;
7363
+ connecting = false;
7364
+ backoff = RETRY_START_MS;
7365
+ opened.socket.once("close", () => {
7366
+ clearConnection();
7367
+ schedule();
7368
+ });
7369
+ heartbeat = setInterval(() => {
7370
+ try {
7371
+ session?.ping();
7372
+ } catch {
7373
+ }
7374
+ }, 3e4);
7375
+ heartbeat.unref();
7376
+ void drainOutbox();
7377
+ } catch (error) {
7378
+ connecting = false;
7379
+ debug2(`retry: ${error.message}`);
7380
+ schedule();
7381
+ }
7382
+ };
7383
+ const drainOutbox = async () => {
7384
+ if (draining || !session) return;
7385
+ draining = true;
7386
+ try {
7387
+ const rows = pendingReceptionOutbox(context2.appHome);
7388
+ await Promise.all(
7389
+ rows.map(async (row) => {
7390
+ try {
7391
+ const active = session;
7392
+ if (!active || !metadata2.has(row.roadId)) throw new Error("connection_not_available");
7393
+ await active.sendRoadText(
7394
+ row.roadId,
7395
+ encodePersonMessage({
7396
+ kind: row.kind,
7397
+ text: row.body,
7398
+ inReplyTo: row.inReplyTo
7399
+ }),
7400
+ {
7401
+ messageId: row.messageId,
7402
+ onAccepted: () => {
7403
+ markReceptionOutboxSent(context2.appHome, row.messageId);
7404
+ }
7405
+ }
7406
+ );
7407
+ } catch (error) {
7408
+ markReceptionOutboxFailed(context2.appHome, row.messageId, row.attemptCount, error);
7409
+ }
7410
+ })
7411
+ );
7412
+ } finally {
7413
+ draining = false;
7414
+ }
7415
+ };
7416
+ return {
7417
+ start: () => {
7418
+ void connect();
7419
+ refreshTimer = setInterval(() => {
7420
+ if (!releaseLease) {
7421
+ void connect();
7422
+ return;
7423
+ }
7424
+ void refreshDirectory().then((roads2) => {
7425
+ if (roads2.length && !session) void connect();
7426
+ }).catch((error) => debug2(`directory refresh failed: ${error.message}`));
7427
+ }, DIRECTORY_REFRESH_MS);
7428
+ refreshTimer.unref();
7429
+ outboxTimer = setInterval(() => void drainOutbox(), OUTBOX_INTERVAL_MS);
7430
+ outboxTimer.unref();
7431
+ },
7432
+ close: () => {
7433
+ stopped = true;
7434
+ if (retryTimer) clearTimeout(retryTimer);
7435
+ if (refreshTimer) clearInterval(refreshTimer);
7436
+ if (outboxTimer) clearInterval(outboxTimer);
7437
+ if (heartbeat) clearInterval(heartbeat);
7438
+ retryTimer = null;
7439
+ refreshTimer = null;
7440
+ outboxTimer = null;
7441
+ heartbeat = null;
7442
+ try {
7443
+ session?.close();
7444
+ } catch {
7445
+ }
7446
+ try {
7447
+ socket?.close();
7448
+ } catch {
7449
+ }
7450
+ clearConnection();
7451
+ releaseLease?.();
7452
+ releaseLease = null;
7453
+ }
7454
+ };
7455
+ }
7456
+ function receptionEndpoint(ownerPrefix, deviceId) {
7457
+ return `${ownerPrefix}/rx-${deviceId.replaceAll("-", "").slice(0, 12)}`;
7458
+ }
7459
+ function receptionEnvelope(deviceId, endpoint, message, road) {
7460
+ if (!road?.connectionId || road.kind !== "connection") {
7461
+ throw new Error("unknown_connection_road");
7462
+ }
7463
+ const person = decodePersonMessage(message.text);
7464
+ return {
7465
+ protocol: BUS_PROTOCOL,
7466
+ id: `managed_${message.messageId.replaceAll("-", "")}`,
7467
+ kind: "road.message",
7468
+ scope: "road",
7469
+ thread: null,
7470
+ from: { city: message.from, actor: "seat", role: "external-seat" },
7471
+ to: { city: endpoint, actor: "seat" },
7472
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7473
+ payload: {
7474
+ text: person.text,
7475
+ trust: "information-not-authority",
7476
+ transport: "managed-e2ee",
7477
+ messageKind: person.kind,
7478
+ inReplyTo: person.inReplyTo,
7479
+ sourceName: road.peerName,
7480
+ connectionId: road.connectionId,
7481
+ roadId: road.id,
7482
+ remoteMessageId: message.messageId,
7483
+ receiverDeviceId: deviceId
7484
+ }
7485
+ };
7486
+ }
7487
+ function tryAcquireReceptionLease(appHome) {
7488
+ const lock = join12(connectStateDirectory(appHome), "reception.lock");
7489
+ const owner = process.pid;
7490
+ for (let attempt = 0; attempt < 2; attempt += 1) {
7491
+ try {
7492
+ const fd = openSync4(lock, "wx", 384);
7493
+ try {
7494
+ writeFileSync7(fd, `${owner}
7495
+ `);
7496
+ } finally {
7497
+ closeSync4(fd);
7498
+ }
7499
+ return () => {
7500
+ if (lockOwner2(lock) !== owner) return;
7501
+ try {
7502
+ unlinkSync5(lock);
7503
+ } catch {
7504
+ }
7505
+ };
7506
+ } catch (error) {
7507
+ if (error.code !== "EEXIST") throw error;
7508
+ const info = lstatSync3(lock);
7509
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error("unsafe_reception_lock");
7510
+ const old = lockOwner2(lock);
7511
+ if (old > 0 && processAlive2(old) || !old && Date.now() - statSync4(lock).mtimeMs < 5e3) {
7512
+ return null;
7513
+ }
7514
+ try {
7515
+ unlinkSync5(lock);
7516
+ } catch (unlinkError) {
7517
+ if (unlinkError.code !== "ENOENT") throw unlinkError;
7518
+ }
7519
+ }
7520
+ }
7521
+ return null;
7522
+ }
7523
+ function lockOwner2(path) {
7524
+ if (!existsSync8(path)) return 0;
7525
+ try {
7526
+ return Number(readFileSync9(path, "utf8").trim()) || 0;
7527
+ } catch {
7528
+ return 0;
7529
+ }
7530
+ }
7531
+ function processAlive2(pid) {
7532
+ try {
7533
+ process.kill(pid, 0);
7534
+ return true;
7535
+ } catch {
7536
+ return false;
7537
+ }
7538
+ }
7539
+
5638
7540
  // hub/road-controller.ts
5639
7541
  function roadController(context2, router2) {
5640
- const inbound = (envelope) => {
5641
- const road = context2.roads.find((candidate) => candidate.address === envelope.from.city);
7542
+ const wakeIntervalMs = duration(
7543
+ process.env.CITY_ROAD_INBOX_WAKE_INTERVAL_MS,
7544
+ 5 * 6e4,
7545
+ 3e4,
7546
+ 60 * 6e4
7547
+ );
7548
+ const wakeCheckMs = Math.min(3e4, wakeIntervalMs);
7549
+ const receptionCheckMs = duration(
7550
+ process.env.CITY_RECEPTION_DELIVERY_INTERVAL_MS,
7551
+ 1e3,
7552
+ 250,
7553
+ 3e4
7554
+ );
7555
+ let wakeTimer = null;
7556
+ let receptionTimer = null;
7557
+ let remote;
7558
+ const reception = managedReceptionBridge(context2);
7559
+ const roads2 = () => {
7560
+ const merged = [...context2.roads, ...remote?.roads() ?? []];
7561
+ return merged.filter(
7562
+ (road, index) => merged.findIndex((candidate) => candidate.address === road.address) === index
7563
+ );
7564
+ };
7565
+ const validateInbound = (envelope) => {
7566
+ const road = roads2().find((candidate) => candidate.address === envelope.from.city);
5642
7567
  if (!road) throw new Error(`there is no road from ${envelope.from.city}`);
5643
7568
  if (envelope.protocol !== BUS_PROTOCOL || envelope.scope !== "road" || envelope.from.actor !== "seat" || envelope.to.city !== context2.city.address || envelope.to.actor !== "seat") {
5644
7569
  throw new Error("invalid road envelope");
5645
7570
  }
7571
+ };
7572
+ const inbound = (envelope) => {
7573
+ validateInbound(envelope);
7574
+ if (envelope.payload?.transport === "managed-e2ee") {
7575
+ recordReceptionMessage(context2, envelope);
7576
+ return;
7577
+ }
5646
7578
  const rawBody = envelope.payload?.text;
5647
7579
  const guarded = typeof rawBody === "string" ? {
5648
7580
  ...envelope,
@@ -5652,18 +7584,31 @@ function roadController(context2, router2) {
5652
7584
  textRaw: void 0
5653
7585
  }
5654
7586
  } : envelope;
5655
- recordRoadInbox(context2.runtimeDir, guarded);
5656
- router2.roadInbound(guarded);
7587
+ const newlyRecorded = recordRoadInbox(context2.runtimeDir, guarded);
7588
+ notifyBacklog();
7589
+ if (!newlyRecorded) return;
7590
+ markRoadInboxAccepted(context2.runtimeDir, guarded.id);
5657
7591
  };
5658
- const remote = remoteRoadBridge(context2, (envelope) => {
5659
- try {
5660
- inbound(envelope);
5661
- } catch (error) {
5662
- console.error(`[city-bus] dropped remote envelope: ${error.message}`);
7592
+ const inboundManaged = (envelope) => {
7593
+ validateInbound(envelope);
7594
+ if (envelope.payload?.transport !== "managed-e2ee") {
7595
+ throw new Error("managed delivery contains a non-managed envelope");
5663
7596
  }
5664
- });
7597
+ return recordReceptionMessage(context2, envelope);
7598
+ };
7599
+ remote = remoteRoadBridge(context2, inbound, inboundManaged);
7600
+ const notifyBacklog = () => {
7601
+ const status = roadInboxStatus(context2.runtimeDir);
7602
+ if (!status.pending || Date.now() - status.notifiedAt < wakeIntervalMs) return;
7603
+ router2.internal("road.inbox.ready", "seat", "chair", "seat", null, {
7604
+ pending: status.pending,
7605
+ oldestAt: status.oldestAt,
7606
+ batchSize: ROAD_INBOX_BATCH_SIZE
7607
+ });
7608
+ markRoadInboxNotified(context2.runtimeDir);
7609
+ };
5665
7610
  const sendOne = async (to, body) => {
5666
- const road = context2.roads.find((candidate) => candidate.address === to);
7611
+ const road = roads2().find((candidate) => candidate.address === to);
5667
7612
  if (!road) throw new Error(`no road from ${context2.city.address} to ${to}`);
5668
7613
  const envelope = {
5669
7614
  protocol: BUS_PROTOCOL,
@@ -5681,7 +7626,7 @@ function roadController(context2, router2) {
5681
7626
  const command = async (name, payload, actor, role) => {
5682
7627
  if (name === "road.roster") {
5683
7628
  requireChair(actor, role);
5684
- return context2.roads.map((road) => ({
7629
+ return roads2().map((road) => ({
5685
7630
  ...road,
5686
7631
  online: road.local ? localRoadOnline(context2, road) : remote.online(road.address)
5687
7632
  }));
@@ -5695,12 +7640,49 @@ function roadController(context2, router2) {
5695
7640
  const to = text(payload.to, "to");
5696
7641
  const body = text(payload.text, "text");
5697
7642
  if (to !== "*") return { results: [await sendOne(to, body)] };
5698
- if (!context2.roads.length) throw new Error("this city has no roads");
7643
+ const currentRoads = roads2();
7644
+ if (!currentRoads.length) throw new Error("this city has no roads");
5699
7645
  const results = [];
5700
- for (const road of context2.roads) results.push(await sendOne(road.address, body));
7646
+ for (const road of currentRoads) results.push(await sendOne(road.address, body));
5701
7647
  return { results };
5702
7648
  };
5703
- return { command, inbound, start: remote.start, close: remote.close };
7649
+ const start = () => {
7650
+ remote.start();
7651
+ reception.start();
7652
+ deliverReception();
7653
+ notifyBacklog();
7654
+ receptionTimer = setInterval(deliverReception, receptionCheckMs);
7655
+ receptionTimer.unref();
7656
+ wakeTimer = setInterval(() => {
7657
+ try {
7658
+ notifyBacklog();
7659
+ } catch (error) {
7660
+ console.error(`[city-bus] Road inbox wake-up failed: ${error.message}`);
7661
+ }
7662
+ }, wakeCheckMs);
7663
+ wakeTimer.unref();
7664
+ };
7665
+ const close2 = () => {
7666
+ if (wakeTimer) clearInterval(wakeTimer);
7667
+ if (receptionTimer) clearInterval(receptionTimer);
7668
+ wakeTimer = null;
7669
+ receptionTimer = null;
7670
+ remote.close();
7671
+ reception.close();
7672
+ };
7673
+ return { command, inbound, start, close: close2 };
7674
+ function deliverReception() {
7675
+ try {
7676
+ const result2 = deliverApprovedReception(context2);
7677
+ if (result2.delivered) notifyBacklog();
7678
+ } catch (error) {
7679
+ console.error(`[city-bus] Reception delivery failed: ${error.message}`);
7680
+ }
7681
+ }
7682
+ }
7683
+ function duration(raw, fallback, minimum, maximum) {
7684
+ const value = Number(raw);
7685
+ return Number.isSafeInteger(value) && value >= minimum && value <= maximum ? value : fallback;
5704
7686
  }
5705
7687
 
5706
7688
  // local-hub.ts
@@ -5827,11 +7809,12 @@ server.listen(0, "127.0.0.1", () => {
5827
7809
  };
5828
7810
  publishEndpoint(context, endpoint);
5829
7811
  diagnostics("hub.listening", { outcome: "ready", message: endpoint.url });
5830
- for (const envelope of drainRoadQueue(context.runtimeDir)) {
7812
+ for (const queued of pendingRoadQueue(context.runtimeDir)) {
5831
7813
  try {
5832
- roads.inbound(envelope);
7814
+ roads.inbound(queued.envelope);
7815
+ acknowledgeRoadQueue(queued.queueFile);
5833
7816
  } catch (error) {
5834
- console.error(`[city-bus] dropped queued road envelope: ${error.message}`);
7817
+ console.error(`[city-bus] kept queued Road envelope: ${error.message}`);
5835
7818
  }
5836
7819
  }
5837
7820
  roads.start();
@@ -5942,7 +7925,7 @@ function authenticateActor(url, mode) {
5942
7925
  const actor = url.searchParams.get("actor") || "";
5943
7926
  if (!ACTOR_RE.test(actor) || !context.actors[actor]) throw new Error("unknown city actor");
5944
7927
  const credential = JSON.parse(
5945
- readFileSync8(credentialPath(context, actor), "utf8")
7928
+ readFileSync10(credentialPath(context, actor), "utf8")
5946
7929
  );
5947
7930
  if (!sameSecret(url.searchParams.get("token") || "", credential.token))
5948
7931
  throw new Error("invalid actor token");