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`.
@@ -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~]+$/;
@@ -3704,7 +3708,7 @@ var require_websocket_server = __commonJS({
3704
3708
 
3705
3709
  // hub-client.ts
3706
3710
  import { spawn } from "child_process";
3707
- import { mkdirSync as mkdirSync2, openSync } from "fs";
3711
+ import { mkdirSync as mkdirSync2, openSync as openSync2 } from "fs";
3708
3712
  import { fileURLToPath } from "url";
3709
3713
 
3710
3714
  // node_modules/ws/wrapper.mjs
@@ -3823,17 +3827,50 @@ function escapeRegExp(value) {
3823
3827
 
3824
3828
  // runtime-files.ts
3825
3829
  import { randomBytes } from "crypto";
3826
- import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
3830
+ import {
3831
+ chmodSync,
3832
+ closeSync,
3833
+ constants,
3834
+ existsSync as existsSync2,
3835
+ fsyncSync,
3836
+ mkdirSync,
3837
+ openSync,
3838
+ readFileSync as readFileSync2,
3839
+ renameSync,
3840
+ unlinkSync,
3841
+ writeFileSync
3842
+ } from "fs";
3827
3843
  import { dirname, join as join2 } from "path";
3828
3844
  var counter = 0;
3829
3845
  function atomicJson(path, value) {
3830
- mkdirSync(dirname(path), { recursive: true, mode: 448 });
3846
+ const directory = dirname(path);
3847
+ mkdirSync(directory, { recursive: true, mode: 448 });
3831
3848
  const tmp = `${path}.tmp-${process.pid}-${counter++}`;
3832
- writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
3833
- renameSync(tmp, path);
3834
3849
  try {
3850
+ const fd = openSync(tmp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 384);
3851
+ try {
3852
+ writeFileSync(fd, JSON.stringify(value, null, 2) + "\n");
3853
+ fsyncSync(fd);
3854
+ } finally {
3855
+ closeSync(fd);
3856
+ }
3857
+ renameSync(tmp, path);
3835
3858
  chmodSync(path, 384);
3836
- } catch {
3859
+ try {
3860
+ const dirFd = openSync(directory, constants.O_RDONLY);
3861
+ try {
3862
+ fsyncSync(dirFd);
3863
+ } finally {
3864
+ closeSync(dirFd);
3865
+ }
3866
+ } catch {
3867
+ }
3868
+ } catch (error) {
3869
+ try {
3870
+ unlinkSync(tmp);
3871
+ } catch {
3872
+ }
3873
+ throw error;
3837
3874
  }
3838
3875
  }
3839
3876
  function actorCredential(context, actor) {
@@ -3879,7 +3916,7 @@ async function ensureHub(context = loadCityContext()) {
3879
3916
  let endpoint = readEndpoint(context);
3880
3917
  if (endpoint && await healthy(endpoint)) return endpoint;
3881
3918
  const hub = fileURLToPath(new URL("./local-hub.js", import.meta.url));
3882
- const log = openSync(`${context.runtimeDir}/hub.log`, "a", 384);
3919
+ const log = openSync2(`${context.runtimeDir}/hub.log`, "a", 384);
3883
3920
  const child = spawn(process.execPath, [hub, "--data", context.dataDir], {
3884
3921
  detached: true,
3885
3922
  stdio: ["ignore", log, log],
@@ -4155,7 +4192,7 @@ async function roadBusCli(args) {
4155
4192
  if (["help", "-h", "--help"].includes(verb)) {
4156
4193
  return {
4157
4194
  usage: "agents-city bus <roster | inbox | send owner/city text>",
4158
- note: "Only the seat can cross a road; repo agents are rejected by the bus ACL."
4195
+ note: "Only the seat can cross a road. Inbox returns a bounded batch so a burst never becomes one model turn per message."
4159
4196
  };
4160
4197
  }
4161
4198
  if (verb === "roster") return busCommand("road.roster");
@@ -1,13 +1,43 @@
1
- import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync } from 'fs';
1
+ import {
2
+ appendFileSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ readdirSync,
7
+ statSync,
8
+ unlinkSync,
9
+ } from 'fs';
2
10
  import { join } from 'path';
3
11
  import { atomicJson } from './runtime-files.js';
4
12
  import { BusEnvelope, MAX_PENDING, MESSAGE_TTL_MS, safeSegment } from './protocol.js';
5
13
 
14
+ export const ROAD_INBOX_BATCH_SIZE = 20;
15
+ const DEFAULT_ROAD_INBOX_LIMIT = 500;
16
+ const MAX_ROAD_INBOX_LIMIT = 10_000;
17
+ const ROAD_INBOX_WAKE_FILE = 'road-inbox-wakeup.json';
18
+
19
+ export interface RoadInboxStatus {
20
+ pending: number;
21
+ oldestAt: string | null;
22
+ notifiedAt: number;
23
+ }
24
+
25
+ export interface RoadInboxBatch {
26
+ messages: BusEnvelope[];
27
+ remaining: number;
28
+ }
29
+
30
+ export interface PendingRoadDelivery {
31
+ envelope: BusEnvelope;
32
+ queueFile: string;
33
+ }
34
+
6
35
  export function enqueueForActor(runtimeDir: string, envelope: BusEnvelope): void {
7
36
  const directory = join(runtimeDir, 'outbox', safeSegment(envelope.to.actor));
8
37
  mkdirSync(directory, { recursive: true, mode: 0o700 });
9
- trim(directory);
10
- atomicJson(join(directory, `${fileKey(envelope.id)}.json`), envelope);
38
+ const path = join(directory, `${fileKey(envelope.id)}.json`);
39
+ requireCapacity(directory, path, MAX_PENDING, 'actor_outbox_full');
40
+ atomicJson(path, envelope);
11
41
  }
12
42
 
13
43
  export function pendingForActor(runtimeDir: string, actor: string): BusEnvelope[] {
@@ -45,18 +75,23 @@ export function acknowledge(runtimeDir: string, actor: string, envelopeId: strin
45
75
  export function queueRoad(runtimeDir: string, envelope: BusEnvelope): void {
46
76
  const directory = join(runtimeDir, 'road-queue');
47
77
  mkdirSync(directory, { recursive: true, mode: 0o700 });
48
- trim(directory);
49
- atomicJson(join(directory, `${fileKey(envelope.id)}.json`), envelope);
78
+ const path = join(directory, `${fileKey(envelope.id)}.json`);
79
+ requireCapacity(directory, path, MAX_PENDING, 'road_queue_full');
80
+ atomicJson(path, envelope);
50
81
  }
51
82
 
52
- export function drainRoadQueue(runtimeDir: string): BusEnvelope[] {
83
+ export function pendingRoadQueue(runtimeDir: string): PendingRoadDelivery[] {
53
84
  const directory = join(runtimeDir, 'road-queue');
54
- const out: BusEnvelope[] = [];
55
- for (const path of jsonFiles(directory)) {
85
+ const out: PendingRoadDelivery[] = [];
86
+ for (const path of jsonFilesByAge(directory)) {
56
87
  try {
57
88
  const envelope = JSON.parse(readFileSync(path, 'utf8')) as BusEnvelope;
58
- if (Date.now() - Date.parse(envelope.createdAt) <= MESSAGE_TTL_MS) out.push(envelope);
89
+ if (Date.now() - Date.parse(envelope.createdAt) <= MESSAGE_TTL_MS) {
90
+ out.push({ envelope, queueFile: path });
91
+ continue;
92
+ }
59
93
  } catch {}
94
+ // Malformed and explicitly expired queue entries are not retryable.
60
95
  try {
61
96
  unlinkSync(path);
62
97
  } catch {}
@@ -64,20 +99,75 @@ export function drainRoadQueue(runtimeDir: string): BusEnvelope[] {
64
99
  return out;
65
100
  }
66
101
 
67
- export function recordRoadInbox(runtimeDir: string, envelope: BusEnvelope): void {
102
+ export function acknowledgeRoadQueue(queueFile: string): void {
103
+ try {
104
+ unlinkSync(queueFile);
105
+ } catch {}
106
+ }
107
+
108
+ export function recordRoadInbox(runtimeDir: string, envelope: BusEnvelope): boolean {
68
109
  const directory = join(runtimeDir, 'road-inbox');
110
+ const receipts = join(runtimeDir, 'road-receipts');
69
111
  mkdirSync(directory, { recursive: true, mode: 0o700 });
70
- trim(directory);
71
- atomicJson(join(directory, `${fileKey(envelope.id)}.json`), envelope);
72
- appendFileSync(join(runtimeDir, 'road-history.jsonl'), JSON.stringify(envelope) + '\n', {
73
- mode: 0o600,
74
- });
112
+ mkdirSync(receipts, { recursive: true, mode: 0o700 });
113
+ const key = fileKey(envelope.id);
114
+ const receipt = join(receipts, `${key}.json`);
115
+ if (existsSync(receipt)) return false;
116
+ const inbox = join(directory, `${key}.json`);
117
+ // A crash before markRoadInboxAccepted may replay the relay frame. Preserve
118
+ // the one inbox record and let the router idempotently recreate its outbox
119
+ // entry. The remote ACK is sent only after that durable handoff completes.
120
+ const recovered = existsSync(inbox);
121
+ if (!recovered) {
122
+ requireCapacity(directory, inbox, roadInboxLimit(), 'road_inbox_full');
123
+ atomicJson(inbox, envelope);
124
+ appendFileSync(join(runtimeDir, 'road-history.jsonl'), JSON.stringify(envelope) + '\n', {
125
+ mode: 0o600,
126
+ });
127
+ }
128
+ return true;
129
+ }
130
+
131
+ export function markRoadInboxAccepted(runtimeDir: string, envelopeId: string): void {
132
+ const receipts = join(runtimeDir, 'road-receipts');
133
+ mkdirSync(receipts, { recursive: true, mode: 0o700 });
134
+ const key = fileKey(envelopeId);
135
+ const receipt = join(receipts, `${key}.json`);
136
+ if (existsSync(receipt)) return;
137
+ trimTo(receipts, 1_000);
138
+ atomicJson(receipt, { id: envelopeId, acceptedAt: new Date().toISOString() });
75
139
  }
76
140
 
77
- export function takeRoadInbox(runtimeDir: string): BusEnvelope[] {
141
+ export function roadInboxStatus(runtimeDir: string): RoadInboxStatus {
142
+ const directory = join(runtimeDir, 'road-inbox');
143
+ const paths = jsonFilesByAge(directory);
144
+ let notifiedAt = 0;
145
+ try {
146
+ const state = JSON.parse(readFileSync(join(runtimeDir, ROAD_INBOX_WAKE_FILE), 'utf8')) as {
147
+ notifiedAt?: number;
148
+ };
149
+ if (Number.isSafeInteger(state.notifiedAt) && Number(state.notifiedAt) > 0) {
150
+ notifiedAt = Number(state.notifiedAt);
151
+ }
152
+ } catch {}
153
+ return {
154
+ pending: paths.length,
155
+ oldestAt: oldestTimestamp(paths),
156
+ notifiedAt,
157
+ };
158
+ }
159
+
160
+ export function markRoadInboxNotified(runtimeDir: string, notifiedAt = Date.now()): void {
161
+ atomicJson(join(runtimeDir, ROAD_INBOX_WAKE_FILE), { notifiedAt });
162
+ }
163
+
164
+ export function takeRoadInbox(runtimeDir: string, limit = ROAD_INBOX_BATCH_SIZE): RoadInboxBatch {
165
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > ROAD_INBOX_BATCH_SIZE) {
166
+ throw new Error('invalid_road_inbox_batch_size');
167
+ }
78
168
  const directory = join(runtimeDir, 'road-inbox');
79
169
  const out: BusEnvelope[] = [];
80
- for (const path of jsonFiles(directory)) {
170
+ for (const path of jsonFilesByAge(directory).slice(0, limit)) {
81
171
  try {
82
172
  out.push(JSON.parse(readFileSync(path, 'utf8')) as BusEnvelope);
83
173
  } catch {}
@@ -85,7 +175,13 @@ export function takeRoadInbox(runtimeDir: string): BusEnvelope[] {
85
175
  unlinkSync(path);
86
176
  } catch {}
87
177
  }
88
- return out;
178
+ const remaining = jsonFiles(directory).length;
179
+ if (!remaining) {
180
+ try {
181
+ unlinkSync(join(runtimeDir, ROAD_INBOX_WAKE_FILE));
182
+ } catch {}
183
+ }
184
+ return { messages: out, remaining };
89
185
  }
90
186
 
91
187
  function jsonFiles(directory: string): string[] {
@@ -100,6 +196,30 @@ function jsonFiles(directory: string): string[] {
100
196
  }
101
197
  }
102
198
 
199
+ function jsonFilesByAge(directory: string): string[] {
200
+ return jsonFiles(directory).sort((left, right) => {
201
+ try {
202
+ const delta = statSync(left).mtimeMs - statSync(right).mtimeMs;
203
+ return delta || left.localeCompare(right);
204
+ } catch {
205
+ return left.localeCompare(right);
206
+ }
207
+ });
208
+ }
209
+
210
+ function oldestTimestamp(paths: string[]): string | null {
211
+ for (const path of paths) {
212
+ try {
213
+ return new Date(statSync(path).mtimeMs).toISOString();
214
+ } catch {
215
+ // A concurrent inbox read may remove the oldest entry between listing
216
+ // and stat. Continue to the next surviving item instead of failing a
217
+ // Road delivery that was already durably accepted.
218
+ }
219
+ }
220
+ return null;
221
+ }
222
+
103
223
  function fileKey(value: string): string {
104
224
  const out = String(value)
105
225
  .replace(/[^a-zA-Z0-9_.-]+/g, '-')
@@ -108,9 +228,28 @@ function fileKey(value: string): string {
108
228
  return out;
109
229
  }
110
230
 
111
- function trim(directory: string): void {
112
- const files = jsonFiles(directory);
113
- for (const path of files.slice(0, Math.max(0, files.length - MAX_PENDING + 1))) {
231
+ function roadInboxLimit(): number {
232
+ const configured = Number(process.env.CITY_ROAD_INBOX_MAX_PENDING);
233
+ return Number.isSafeInteger(configured) && configured >= ROAD_INBOX_BATCH_SIZE
234
+ ? Math.min(configured, MAX_ROAD_INBOX_LIMIT)
235
+ : DEFAULT_ROAD_INBOX_LIMIT;
236
+ }
237
+
238
+ function requireCapacity(directory: string, target: string, maximum: number, code: string): void {
239
+ if (existsSync(target)) return;
240
+ if (jsonFiles(directory).length >= maximum) throw new Error(code);
241
+ }
242
+
243
+ function trimTo(directory: string, maximum: number): void {
244
+ const files = jsonFiles(directory).sort((left, right) => {
245
+ try {
246
+ const delta = statSync(left).mtimeMs - statSync(right).mtimeMs;
247
+ return delta || left.localeCompare(right);
248
+ } catch {
249
+ return left.localeCompare(right);
250
+ }
251
+ });
252
+ for (const path of files.slice(0, Math.max(0, files.length - maximum + 1))) {
114
253
  try {
115
254
  unlinkSync(path);
116
255
  } catch {}
@@ -1,8 +1,9 @@
1
1
  import WebSocket from 'ws';
2
2
  import { CityContext } from '../city-config.js';
3
3
  import { BUS_PROTOCOL, BusEnvelope, isoNow, randomId } from '../protocol.js';
4
+ import { managedRoadBridge } from '../managed-connect/bridge.js';
4
5
 
5
- export function remoteRoadBridge(context: CityContext, receive: (envelope: BusEnvelope) => void) {
6
+ function legacyRemoteRoadBridge(context: CityContext, receive: (envelope: BusEnvelope) => void) {
6
7
  const base = process.env.CITY_BUS_URL || '';
7
8
  const token = process.env.CITY_BUS_TOKEN || '';
8
9
  const enabled = Boolean(base && token);
@@ -148,6 +149,36 @@ export function remoteRoadBridge(context: CityContext, receive: (envelope: BusEn
148
149
  };
149
150
  }
150
151
 
152
+ export function remoteRoadBridge(
153
+ context: CityContext,
154
+ receive: (envelope: BusEnvelope) => void,
155
+ receiveManaged?: (envelope: BusEnvelope) => { inserted: boolean },
156
+ ) {
157
+ const legacy = legacyRemoteRoadBridge(context, (envelope) => {
158
+ try {
159
+ receive(envelope);
160
+ } catch (error) {
161
+ console.error(`[city-bus] dropped remote envelope: ${(error as Error).message}`);
162
+ }
163
+ });
164
+ const managed = managedRoadBridge(context, receive, receiveManaged);
165
+ return {
166
+ start: () => {
167
+ legacy.start();
168
+ managed.start();
169
+ },
170
+ close: () => {
171
+ legacy.close();
172
+ managed.close();
173
+ },
174
+ send: (to: string, envelope: BusEnvelope) =>
175
+ managed.hasRoad(to) ? managed.send(to, envelope) : legacy.send(to, envelope),
176
+ enabled: () => legacy.enabled() || managed.enabled(),
177
+ online: (address: string) => managed.online(address) || legacy.online(address),
178
+ roads: managed.roads,
179
+ };
180
+ }
181
+
151
182
  function legacyEnvelope(context: CityContext, from: string, body: string, id: string): BusEnvelope {
152
183
  return {
153
184
  protocol: BUS_PROTOCOL,
@@ -1,15 +1,48 @@
1
1
  import { ActorRole, BUS_PROTOCOL, BusEnvelope, isoNow, randomId, text } from '../protocol.js';
2
2
  import { CityContext } from '../city-config.js';
3
- import { recordRoadInbox, takeRoadInbox } from '../delivery-queue.js';
3
+ import {
4
+ ROAD_INBOX_BATCH_SIZE,
5
+ markRoadInboxAccepted,
6
+ markRoadInboxNotified,
7
+ recordRoadInbox,
8
+ roadInboxStatus,
9
+ takeRoadInbox,
10
+ } from '../delivery-queue.js';
4
11
  import { requireChair } from '../committee/guards.js';
12
+ import { deliverApprovedReception, recordReceptionMessage } from '../reception.js';
5
13
  import { wrapUntrusted } from '../untrusted.js';
6
14
  import { EnvelopeRouter } from './envelopes.js';
7
15
  import { localRoadOnline, sendLocalRoad } from './local-roads.js';
8
16
  import { remoteRoadBridge } from './remote-roads.js';
17
+ import { managedReceptionBridge } from '../managed-connect/reception-bridge.js';
9
18
 
10
19
  export function roadController(context: CityContext, router: EnvelopeRouter) {
11
- const inbound = (envelope: BusEnvelope): void => {
12
- const road = context.roads.find((candidate) => candidate.address === envelope.from.city);
20
+ const wakeIntervalMs = duration(
21
+ process.env.CITY_ROAD_INBOX_WAKE_INTERVAL_MS,
22
+ 5 * 60_000,
23
+ 30_000,
24
+ 60 * 60_000,
25
+ );
26
+ const wakeCheckMs = Math.min(30_000, wakeIntervalMs);
27
+ const receptionCheckMs = duration(
28
+ process.env.CITY_RECEPTION_DELIVERY_INTERVAL_MS,
29
+ 1_000,
30
+ 250,
31
+ 30_000,
32
+ );
33
+ let wakeTimer: ReturnType<typeof setInterval> | null = null;
34
+ let receptionTimer: ReturnType<typeof setInterval> | null = null;
35
+ let remote: ReturnType<typeof remoteRoadBridge>;
36
+ const reception = managedReceptionBridge(context);
37
+ const roads = () => {
38
+ const merged = [...context.roads, ...(remote?.roads() ?? [])];
39
+ return merged.filter(
40
+ (road, index) =>
41
+ merged.findIndex((candidate) => candidate.address === road.address) === index,
42
+ );
43
+ };
44
+ const validateInbound = (envelope: BusEnvelope): void => {
45
+ const road = roads().find((candidate) => candidate.address === envelope.from.city);
13
46
  if (!road) throw new Error(`there is no road from ${envelope.from.city}`);
14
47
  if (
15
48
  envelope.protocol !== BUS_PROTOCOL ||
@@ -20,6 +53,16 @@ export function roadController(context: CityContext, router: EnvelopeRouter) {
20
53
  ) {
21
54
  throw new Error('invalid road envelope');
22
55
  }
56
+ };
57
+ const inbound = (envelope: BusEnvelope): void => {
58
+ validateInbound(envelope);
59
+ // Managed traffic crosses a human boundary first. The raw text is durable
60
+ // in the owner-level local reception, but neither this city nor any model
61
+ // can read it through road.inbox until the Hall routes it explicitly.
62
+ if (envelope.payload?.transport === 'managed-e2ee') {
63
+ recordReceptionMessage(context, envelope);
64
+ return;
65
+ }
23
66
  // The body is untrusted text from another city: wrap it in an unforgeable
24
67
  // boundary and defang chat-template tokens before it can reach the seat's
25
68
  // context. Done once, here, so every inbound path (local and remote) and
@@ -37,19 +80,35 @@ export function roadController(context: CityContext, router: EnvelopeRouter) {
37
80
  },
38
81
  }
39
82
  : envelope;
40
- recordRoadInbox(context.runtimeDir, guarded);
41
- router.roadInbound(guarded);
83
+ const newlyRecorded = recordRoadInbox(context.runtimeDir, guarded);
84
+ notifyBacklog();
85
+ if (!newlyRecorded) return;
86
+ // The local/self-hosted transport may now return success: the inbox record
87
+ // and deduplication receipt are durable under one stable envelope id.
88
+ markRoadInboxAccepted(context.runtimeDir, guarded.id);
42
89
  };
43
- const remote = remoteRoadBridge(context, (envelope) => {
44
- try {
45
- inbound(envelope);
46
- } catch (error) {
47
- console.error(`[city-bus] dropped remote envelope: ${(error as Error).message}`);
90
+ const inboundManaged = (envelope: BusEnvelope): { inserted: boolean } => {
91
+ validateInbound(envelope);
92
+ if (envelope.payload?.transport !== 'managed-e2ee') {
93
+ throw new Error('managed delivery contains a non-managed envelope');
48
94
  }
49
- });
95
+ return recordReceptionMessage(context, envelope);
96
+ };
97
+ remote = remoteRoadBridge(context, inbound, inboundManaged);
98
+
99
+ const notifyBacklog = (): void => {
100
+ const status = roadInboxStatus(context.runtimeDir);
101
+ if (!status.pending || Date.now() - status.notifiedAt < wakeIntervalMs) return;
102
+ router.internal('road.inbox.ready', 'seat', 'chair', 'seat', null, {
103
+ pending: status.pending,
104
+ oldestAt: status.oldestAt,
105
+ batchSize: ROAD_INBOX_BATCH_SIZE,
106
+ });
107
+ markRoadInboxNotified(context.runtimeDir);
108
+ };
50
109
 
51
110
  const sendOne = async (to: string, body: string): Promise<string> => {
52
- const road = context.roads.find((candidate) => candidate.address === to);
111
+ const road = roads().find((candidate) => candidate.address === to);
53
112
  if (!road) throw new Error(`no road from ${context.city.address} to ${to}`);
54
113
  const envelope: BusEnvelope = {
55
114
  protocol: BUS_PROTOCOL,
@@ -73,7 +132,7 @@ export function roadController(context: CityContext, router: EnvelopeRouter) {
73
132
  ): Promise<unknown> => {
74
133
  if (name === 'road.roster') {
75
134
  requireChair(actor, role);
76
- return context.roads.map((road) => ({
135
+ return roads().map((road) => ({
77
136
  ...road,
78
137
  online: road.local ? localRoadOnline(context, road) : remote.online(road.address),
79
138
  }));
@@ -87,13 +146,54 @@ export function roadController(context: CityContext, router: EnvelopeRouter) {
87
146
  const to = text(payload.to, 'to');
88
147
  const body = text(payload.text, 'text');
89
148
  if (to !== '*') return { results: [await sendOne(to, body)] };
90
- if (!context.roads.length) throw new Error('this city has no roads');
149
+ const currentRoads = roads();
150
+ if (!currentRoads.length) throw new Error('this city has no roads');
91
151
  const results: string[] = [];
92
- for (const road of context.roads) results.push(await sendOne(road.address, body));
152
+ for (const road of currentRoads) results.push(await sendOne(road.address, body));
93
153
  return { results };
94
154
  };
95
155
 
96
- return { command, inbound, start: remote.start, close: remote.close };
156
+ const start = (): void => {
157
+ remote.start();
158
+ reception.start();
159
+ deliverReception();
160
+ notifyBacklog();
161
+ receptionTimer = setInterval(deliverReception, receptionCheckMs);
162
+ receptionTimer.unref();
163
+ wakeTimer = setInterval(() => {
164
+ try {
165
+ notifyBacklog();
166
+ } catch (error) {
167
+ console.error(`[city-bus] Road inbox wake-up failed: ${(error as Error).message}`);
168
+ }
169
+ }, wakeCheckMs);
170
+ wakeTimer.unref();
171
+ };
172
+
173
+ const close = (): void => {
174
+ if (wakeTimer) clearInterval(wakeTimer);
175
+ if (receptionTimer) clearInterval(receptionTimer);
176
+ wakeTimer = null;
177
+ receptionTimer = null;
178
+ remote.close();
179
+ reception.close();
180
+ };
181
+
182
+ return { command, inbound, start, close };
183
+
184
+ function deliverReception(): void {
185
+ try {
186
+ const result = deliverApprovedReception(context);
187
+ if (result.delivered) notifyBacklog();
188
+ } catch (error) {
189
+ console.error(`[city-bus] Reception delivery failed: ${(error as Error).message}`);
190
+ }
191
+ }
97
192
  }
98
193
 
99
194
  export type RoadController = ReturnType<typeof roadController>;
195
+
196
+ function duration(raw: string | undefined, fallback: number, minimum: number, maximum: number) {
197
+ const value = Number(raw);
198
+ return Number.isSafeInteger(value) && value >= minimum && value <= maximum ? value : fallback;
199
+ }
@@ -0,0 +1,21 @@
1
+ # Third-party cryptography
2
+
3
+ `@agents-city/connect-client` uses `@kinsh/vodozemac-wasm` 0.4.2 for its
4
+ Olm Double Ratchet implementation. The binding and the underlying
5
+ `matrix-org/vodozemac` library are licensed under Apache License 2.0.
6
+
7
+ - Binding source: https://github.com/Meneleus/vodozemac-bindings
8
+ - Cryptographic implementation: https://github.com/matrix-org/vodozemac
9
+ - Independent vodozemac audit: https://leastauthority.com/blog/audit-of-vodozemac-library-for-matrix-org/
10
+ - Licence text: `licenses/vodozemac-Apache-2.0.txt`
11
+
12
+ No `libsignal` source or binary is linked, copied, vendored, or distributed.
13
+ Signal's published protocol documents are used only as a security-properties
14
+ reference. Agents City retains its Apache-2.0 licensing boundary.
15
+
16
+ Persistent Node clients use `@napi-rs/keyring` 1.3.0 to keep the ratchet-state
17
+ master key in macOS Keychain, Windows Credential Manager, or Linux Secret
18
+ Service. The package is distributed under the MIT License.
19
+
20
+ - Source: https://github.com/Brooooooklyn/keyring-node
21
+ - Licence text: `licenses/keyring-MIT.txt`
@@ -0,0 +1,12 @@
1
+ # Third-party notices
2
+
3
+ - `libcrux-ml-kem 0.0.10`, Apache-2.0. ML-KEM-768 implementation from Cryspen/libcrux.
4
+ - `aes-gcm 0.10.3`, Apache-2.0 OR MIT. RustCrypto authenticated encryption.
5
+ - `hkdf 0.12.4`, Apache-2.0 OR MIT. RustCrypto HKDF.
6
+ - `sha2 0.10.9`, Apache-2.0 OR MIT. RustCrypto SHA-2.
7
+ - `wasm-bindgen 0.2.104`, Apache-2.0 OR MIT.
8
+ - `zeroize 1.9.0`, Apache-2.0 OR MIT.
9
+ - `serde_json 1.0.151`, Apache-2.0 OR MIT. Test-only NIST vector parser.
10
+
11
+ Exact resolved transitive versions are pinned by `Cargo.lock`; licence
12
+ validation is a blocking release gate.