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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.es.md +240 -11
- package/README.md +238 -11
- package/benchmarks/latency/fake-claude-cli.mjs +2 -1
- package/benchmarks/reception/README.md +16 -0
- package/benchmarks/reception/run.mjs +84 -0
- package/bin/agents-city.js +1 -0
- package/bin/connect +5 -0
- package/bin/hall.html +117 -18
- package/bin/navegador.mjs +170 -1
- package/bin/serve.py +201 -101
- package/bin/test +15 -3
- package/bin/test-arnes.py +192 -0
- package/bin/test-cage.py +9 -0
- package/bin/test-channel.py +178 -12
- package/bin/test-claude-runtime.py +132 -4
- package/bin/test-connect-client.mjs +609 -0
- package/bin/test-connect.py +52 -0
- package/bin/test-contracts.py +214 -0
- package/bin/test-diario.py +185 -0
- package/bin/test-doctor.py +31 -0
- package/bin/test-i18n.py +189 -0
- package/bin/test-navegador.py +61 -0
- package/bin/test-seat.py +78 -20
- package/bin/test-security.py +2 -0
- package/bin/test-serve.py +366 -0
- package/city/web/dist-hall/hall.js +789 -214
- package/city/web/src/bienvenida.ts +14 -13
- package/city/web/src/casa.ts +143 -56
- package/city/web/src/demo.ts +61 -31
- package/city/web/src/es.ts +187 -27
- package/city/web/src/explorador.ts +70 -43
- package/city/web/src/hall.ts +520 -103
- package/city/web/src/idioma.ts +21 -1
- package/city/web/src/motores.ts +48 -1
- package/city/web/src/vista.ts +58 -0
- package/demo/graba.py +5 -10
- package/docs/managed-connect.md +237 -0
- package/docs/security.md +41 -2
- package/package.json +8 -4
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/channel/adapter-prompts.ts +10 -0
- package/plugin/channel/adapter.js +78 -31
- package/plugin/channel/agents_city_hybrid_crypto_bg.wasm +0 -0
- package/plugin/channel/bus.js +109 -65
- package/plugin/channel/bus.ts +11 -1
- package/plugin/channel/client.js +67 -30
- package/plugin/channel/delivery-queue.ts +160 -21
- package/plugin/channel/hub/remote-roads.ts +32 -1
- package/plugin/channel/hub/road-controller.ts +116 -16
- package/plugin/channel/kinsh_vodozemac_wasm_bg.wasm +0 -0
- package/plugin/channel/licenses/CONNECT_CLIENT_THIRD_PARTY_NOTICES.md +21 -0
- package/plugin/channel/licenses/HYBRID_CRYPTO_THIRD_PARTY_NOTICES.md +12 -0
- package/plugin/channel/licenses/hybrid-crypto-Apache-2.0.txt +201 -0
- package/plugin/channel/licenses/keyring-MIT.txt +21 -0
- package/plugin/channel/licenses/vodozemac-Apache-2.0.txt +201 -0
- package/plugin/channel/local-hub.js +2080 -97
- package/plugin/channel/local-hub.ts +5 -4
- package/plugin/channel/managed-connect/bridge.ts +208 -0
- package/plugin/channel/managed-connect/cli.ts +416 -0
- package/plugin/channel/managed-connect/device.ts +15 -0
- package/plugin/channel/managed-connect/local-cities.ts +94 -0
- package/plugin/channel/managed-connect/person-message.ts +74 -0
- package/plugin/channel/managed-connect/reception-bridge.ts +341 -0
- package/plugin/channel/managed-connect/relay-session.ts +7 -0
- package/plugin/channel/managed-connect/storage.ts +653 -0
- package/plugin/channel/managed-connect/transport.ts +87 -0
- package/plugin/channel/managed-connect-cli.js +4777 -0
- package/plugin/channel/managed-connect-cli.ts +7 -0
- package/plugin/channel/managed-connect-client.d.ts +261 -0
- package/plugin/channel/managed-connect-client.js +6572 -0
- package/plugin/channel/managed-connect-client.manifest.json +38 -0
- package/plugin/channel/package-lock.json +123 -105
- package/plugin/channel/package.json +4 -4
- package/plugin/channel/protocol.ts +4 -0
- package/plugin/channel/reception.ts +896 -0
- package/plugin/channel/road-cli.ts +1 -1
- package/plugin/channel/runtime/arnes.json +189 -0
- package/plugin/channel/runtime/arnes.ts +47 -0
- package/plugin/channel/runtime/claude.ts +18 -0
- package/plugin/channel/runtime/codex-config.ts +51 -1
- package/plugin/channel/runtime/codex.ts +31 -11
- package/plugin/channel/runtime/kimi.ts +6 -4
- package/plugin/channel/runtime-files.ts +39 -5
- package/plugin/channel/runtime-gateway.js +426 -100
- package/plugin/channel/runtime-gateway.ts +12 -0
- package/plugin/channel/trust/agents-city-sandbox-roots.json +66 -0
- package/plugin/scripts/arnes.py +271 -0
- package/plugin/scripts/busca.py +46 -10
- package/plugin/scripts/cage.py +5 -0
- package/plugin/scripts/city-session.sh +144 -28
- package/plugin/scripts/crecimiento.py +4 -1
- package/plugin/scripts/demos.py +58 -43
- package/plugin/scripts/desinstala.py +36 -21
- package/plugin/scripts/diario.py +119 -0
- package/plugin/scripts/doctor.py +90 -15
- package/plugin/scripts/read-card.py +48 -8
- package/plugin/scripts/reception.py +665 -0
package/plugin/channel/client.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 {
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
|
83
|
+
export function pendingRoadQueue(runtimeDir: string): PendingRoadDelivery[] {
|
|
53
84
|
const directory = join(runtimeDir, 'road-queue');
|
|
54
|
-
const out:
|
|
55
|
-
for (const path of
|
|
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)
|
|
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
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
112
|
-
const
|
|
113
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
152
|
+
for (const road of currentRoads) results.push(await sendOne(road.address, body));
|
|
93
153
|
return { results };
|
|
94
154
|
};
|
|
95
155
|
|
|
96
|
-
|
|
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
|
+
}
|
|
Binary file
|
|
@@ -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.
|