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
@@ -0,0 +1,94 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
3
+ import { basename, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ export type LocalCity = {
7
+ current: boolean;
8
+ slug: string;
9
+ id: string;
10
+ dataDir: string;
11
+ name: string;
12
+ };
13
+
14
+ const locatedCitiesScript = [
15
+ new URL('../scripts/cities.py', import.meta.url),
16
+ new URL('../../scripts/cities.py', import.meta.url),
17
+ ]
18
+ .map((url) => fileURLToPath(url))
19
+ .find(existsSync);
20
+
21
+ if (!locatedCitiesScript) throw new Error('Agents City city catalogue script is missing');
22
+ const citiesScript: string = locatedCitiesScript;
23
+
24
+ function scalar(input: string, key: string): string {
25
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
26
+ const match = input.match(new RegExp(`^${escaped}:[ \\t]*(.+)$`, 'm'));
27
+ return match?.[1]?.trim().replace(/^['"]|['"]$/g, '') || '';
28
+ }
29
+
30
+ export function discoverLocalCities(): LocalCity[] {
31
+ const result = spawnSync('python3', [citiesScript, 'list'], {
32
+ encoding: 'utf8',
33
+ env: process.env,
34
+ maxBuffer: 1024 * 1024,
35
+ });
36
+ if (result.status !== 0) {
37
+ throw new Error(result.stderr.trim() || 'could not list local cities');
38
+ }
39
+ const cities = result.stdout
40
+ .split(/\r?\n/)
41
+ .filter(Boolean)
42
+ .map((line) => {
43
+ const [marker, slug, id, rawPath] = line.split('\t');
44
+ if (!slug || !id || !rawPath) throw new Error('invalid local city catalogue');
45
+ const dataDir = realpathSync(rawPath);
46
+ const identity = readFileSync(join(dataDir, 'city.yml'), 'utf8');
47
+ return {
48
+ current: marker === '*',
49
+ slug,
50
+ id,
51
+ dataDir,
52
+ name: scalar(identity, 'name') || basename(dataDir),
53
+ };
54
+ });
55
+ if (new Set(cities.map((city) => city.id)).size !== cities.length) {
56
+ throw new Error('duplicate local city identity');
57
+ }
58
+ return cities;
59
+ }
60
+
61
+ export function selectLocalCities(
62
+ cities: LocalCity[],
63
+ selectors: string[],
64
+ all: boolean,
65
+ ): LocalCity[] {
66
+ if (all && selectors.length) throw new Error('use either --all or --city, not both');
67
+ if (all) return cities;
68
+ if (!selectors.length) {
69
+ const current = cities.find((city) => city.current) ?? cities[0];
70
+ if (!current) throw new Error('there is no local city to connect');
71
+ return [current];
72
+ }
73
+ const selected: LocalCity[] = [];
74
+ for (const raw of selectors) {
75
+ let real = '';
76
+ try {
77
+ real = realpathSync(raw);
78
+ } catch {}
79
+ const query = raw.toLowerCase();
80
+ const matches = cities.filter(
81
+ (city) =>
82
+ city.slug.toLowerCase() === query ||
83
+ city.id.toLowerCase() === query ||
84
+ city.name.toLowerCase() === query ||
85
+ basename(city.dataDir).toLowerCase() === query ||
86
+ (real && city.dataDir === real),
87
+ );
88
+ if (matches.length !== 1) {
89
+ throw new Error(matches.length ? `ambiguous city: ${raw}` : `no local city called ${raw}`);
90
+ }
91
+ if (!selected.some((city) => city.id === matches[0].id)) selected.push(matches[0]);
92
+ }
93
+ return selected;
94
+ }
@@ -0,0 +1,74 @@
1
+ export const PERSON_MESSAGE_PROTOCOL = 'agents-city-person-message/1' as const;
2
+ const MAX_PERSON_TEXT_BYTES = 11_500;
3
+ const 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;
4
+ const textEncoder = new TextEncoder();
5
+ const textDecoder = new TextDecoder('utf-8', { fatal: true });
6
+ const utf8Length = (value: string) => textEncoder.encode(value).byteLength;
7
+
8
+ export type PersonMessage = {
9
+ kind: 'message' | 'rejection';
10
+ text: string;
11
+ inReplyTo: string | null;
12
+ };
13
+
14
+ export function encodePersonMessage(message: PersonMessage): string {
15
+ validateText(message.text);
16
+ if (!['message', 'rejection'].includes(message.kind)) {
17
+ throw new Error('invalid_person_message_kind');
18
+ }
19
+ if (message.inReplyTo !== null && !UUID_RE.test(message.inReplyTo)) {
20
+ throw new Error('invalid_person_reply_reference');
21
+ }
22
+ return textDecoder.decode(
23
+ textEncoder.encode(
24
+ JSON.stringify({
25
+ protocol: PERSON_MESSAGE_PROTOCOL,
26
+ kind: message.kind,
27
+ text: message.text,
28
+ ...(message.inReplyTo ? { inReplyTo: message.inReplyTo } : {}),
29
+ }),
30
+ ),
31
+ );
32
+ }
33
+
34
+ /**
35
+ * A legacy or deliberately malformed application frame is still harmless
36
+ * untrusted text. Keep it visible to the human instead of creating an
37
+ * unacknowledgeable poison message that the relay would retry forever.
38
+ */
39
+ export function decodePersonMessage(value: string): PersonMessage {
40
+ validateText(value);
41
+ let parsed: unknown;
42
+ try {
43
+ parsed = JSON.parse(value);
44
+ } catch {
45
+ return { kind: 'message', text: value, inReplyTo: null };
46
+ }
47
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
48
+ return { kind: 'message', text: value, inReplyTo: null };
49
+ }
50
+ const record = parsed as Record<string, unknown>;
51
+ const allowed = new Set(['protocol', 'kind', 'text', 'inReplyTo']);
52
+ if (
53
+ record.protocol !== PERSON_MESSAGE_PROTOCOL ||
54
+ !['message', 'rejection'].includes(String(record.kind)) ||
55
+ typeof record.text !== 'string' ||
56
+ !record.text.trim() ||
57
+ utf8Length(record.text) > MAX_PERSON_TEXT_BYTES ||
58
+ (record.inReplyTo !== undefined &&
59
+ (typeof record.inReplyTo !== 'string' || !UUID_RE.test(record.inReplyTo))) ||
60
+ Object.keys(record).some((key) => !allowed.has(key))
61
+ ) {
62
+ return { kind: 'message', text: value, inReplyTo: null };
63
+ }
64
+ return {
65
+ kind: record.kind as PersonMessage['kind'],
66
+ text: record.text,
67
+ inReplyTo: typeof record.inReplyTo === 'string' ? record.inReplyTo : null,
68
+ };
69
+ }
70
+
71
+ function validateText(value: string): void {
72
+ if (typeof value !== 'string' || !value.trim()) throw new Error('person_message_required');
73
+ if (utf8Length(value) > MAX_PERSON_TEXT_BYTES) throw new Error('person_message_too_large');
74
+ }
@@ -0,0 +1,341 @@
1
+ import {
2
+ closeSync,
3
+ existsSync,
4
+ lstatSync,
5
+ openSync,
6
+ readFileSync,
7
+ statSync,
8
+ unlinkSync,
9
+ writeFileSync,
10
+ } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import type WebSocket from 'ws';
13
+ import type { CityContext } from '../city-config.js';
14
+ import { BUS_PROTOCOL, type BusEnvelope } from '../protocol.js';
15
+ import {
16
+ markReceptionOutboxFailed,
17
+ markReceptionOutboxSent,
18
+ pendingReceptionOutbox,
19
+ recordReceptionMessage,
20
+ syncReceptionConnections,
21
+ } from '../reception.js';
22
+ import { listDeviceRoads, type DeviceIdentity, type DeviceRoad } from './device.js';
23
+ import { decodePersonMessage, encodePersonMessage } from './person-message.js';
24
+ import type { ManagedRelaySession, UntrustedRoadText } from './relay-session.js';
25
+ import {
26
+ connectStateDirectory,
27
+ loadConnectIdentity,
28
+ readConnectState,
29
+ refreshStoredTransparency,
30
+ type ConnectedConnectState,
31
+ } from './storage.js';
32
+ import { openManagedRelaySession } from './transport.js';
33
+
34
+ const RETRY_START_MS = 1_000;
35
+ const RETRY_MAX_MS = 30_000;
36
+ const DIRECTORY_REFRESH_MS = 30_000;
37
+ const OUTBOX_INTERVAL_MS = 250;
38
+
39
+ export function managedReceptionBridge(context: Pick<CityContext, 'appHome'>) {
40
+ let releaseLease: (() => void) | null = null;
41
+ let session: ManagedRelaySession | null = null;
42
+ let socket: WebSocket | null = null;
43
+ let stopped = false;
44
+ let connecting = false;
45
+ let retryTimer: ReturnType<typeof setTimeout> | null = null;
46
+ let refreshTimer: ReturnType<typeof setInterval> | null = null;
47
+ let outboxTimer: ReturnType<typeof setInterval> | null = null;
48
+ let heartbeat: ReturnType<typeof setInterval> | null = null;
49
+ let backoff = RETRY_START_MS;
50
+ let draining = false;
51
+ let metadata = new Map<string, DeviceRoad>();
52
+ let identity: DeviceIdentity | null = null;
53
+
54
+ const debug = (message: string) => {
55
+ if (process.env.CITY_BUS_DEBUG === '1') console.error(`[reception] ${message}`);
56
+ };
57
+
58
+ const schedule = (delay = backoff): void => {
59
+ if (stopped || retryTimer) return;
60
+ retryTimer = setTimeout(() => {
61
+ retryTimer = null;
62
+ void connect();
63
+ }, delay);
64
+ retryTimer.unref();
65
+ backoff = Math.min(RETRY_MAX_MS, Math.max(RETRY_START_MS, backoff * 2));
66
+ };
67
+
68
+ const clearConnection = (): void => {
69
+ if (heartbeat) clearInterval(heartbeat);
70
+ heartbeat = null;
71
+ session = null;
72
+ socket = null;
73
+ connecting = false;
74
+ };
75
+
76
+ const identityFor = async (state: ConnectedConnectState) => {
77
+ if (identity?.deviceId === state.device.deviceId) return identity;
78
+ identity = await loadConnectIdentity(state, context.appHome);
79
+ return identity;
80
+ };
81
+
82
+ const refreshDirectory = async (): Promise<DeviceRoad[]> => {
83
+ const state = readConnectState(context.appHome);
84
+ if (!state || state.status !== 'connected') return [];
85
+ const activeIdentity = await identityFor(state);
86
+ const endpoint = receptionEndpoint(activeIdentity.ownerPrefix, activeIdentity.deviceId);
87
+ const directory = await listDeviceRoads(state.serviceUrl, activeIdentity);
88
+ const roads = directory.roads.filter(
89
+ (road) =>
90
+ road.kind === 'connection' && Boolean(road.connectionId) && road.localCity === endpoint,
91
+ );
92
+ metadata = new Map(roads.map((road) => [road.id, road]));
93
+ syncReceptionConnections(
94
+ context.appHome,
95
+ roads.map((road) => ({
96
+ roadId: road.id,
97
+ connectionId: road.connectionId!,
98
+ peerName: road.peerName,
99
+ peerEndpoint: road.peerCity,
100
+ })),
101
+ );
102
+ return roads;
103
+ };
104
+
105
+ const connect = async (): Promise<void> => {
106
+ if (stopped || connecting || session) return;
107
+ const state = readConnectState(context.appHome);
108
+ if (!state || state.status !== 'connected') return schedule(DIRECTORY_REFRESH_MS);
109
+ if (!releaseLease) {
110
+ releaseLease = tryAcquireReceptionLease(context.appHome);
111
+ if (!releaseLease) return schedule(5_000);
112
+ }
113
+ connecting = true;
114
+ try {
115
+ const refreshed = await refreshStoredTransparency(context.appHome);
116
+ if (refreshed.state.status !== 'connected') throw new Error('connect_state_not_connected');
117
+ if (refreshed.refreshWarning) {
118
+ debug(`${refreshed.refreshWarning}; using unexpired cached root`);
119
+ }
120
+ const activeIdentity = await identityFor(refreshed.state);
121
+ const roads = await refreshDirectory();
122
+ if (!roads.length) {
123
+ connecting = false;
124
+ backoff = RETRY_START_MS;
125
+ return schedule(DIRECTORY_REFRESH_MS);
126
+ }
127
+ const endpoint = receptionEndpoint(activeIdentity.ownerPrefix, activeIdentity.deviceId);
128
+ const opened = await openManagedRelaySession(activeIdentity, endpoint, {
129
+ onText: async (message) => {
130
+ const recorded = recordReceptionMessage(
131
+ {
132
+ appHome: context.appHome,
133
+ city: { id: `device_${activeIdentity.deviceId}`, address: endpoint },
134
+ },
135
+ receptionEnvelope(
136
+ activeIdentity.deviceId,
137
+ endpoint,
138
+ message,
139
+ metadata.get(message.roadId),
140
+ ),
141
+ );
142
+ return {
143
+ messageId: message.messageId,
144
+ status: recorded.inserted ? 'inserted' : 'duplicate',
145
+ };
146
+ },
147
+ keyTransparency: refreshed.runtime,
148
+ onSecurityError: (error) => debug(`security refusal: ${error.message}`),
149
+ onLocalError: (error) => debug(`local handoff failed: ${error.message}`),
150
+ });
151
+ if (stopped) {
152
+ opened.session.close();
153
+ return;
154
+ }
155
+ session = opened.session;
156
+ socket = opened.socket;
157
+ connecting = false;
158
+ backoff = RETRY_START_MS;
159
+ opened.socket.once('close', () => {
160
+ clearConnection();
161
+ schedule();
162
+ });
163
+ heartbeat = setInterval(() => {
164
+ try {
165
+ session?.ping();
166
+ } catch {}
167
+ }, 30_000);
168
+ heartbeat.unref();
169
+ void drainOutbox();
170
+ } catch (error) {
171
+ connecting = false;
172
+ debug(`retry: ${(error as Error).message}`);
173
+ schedule();
174
+ }
175
+ };
176
+
177
+ const drainOutbox = async (): Promise<void> => {
178
+ if (draining || !session) return;
179
+ draining = true;
180
+ try {
181
+ const rows = pendingReceptionOutbox(context.appHome);
182
+ await Promise.all(
183
+ rows.map(async (row) => {
184
+ try {
185
+ const active = session;
186
+ if (!active || !metadata.has(row.roadId)) throw new Error('connection_not_available');
187
+ await active.sendRoadText(
188
+ row.roadId,
189
+ encodePersonMessage({
190
+ kind: row.kind,
191
+ text: row.body,
192
+ inReplyTo: row.inReplyTo,
193
+ }),
194
+ {
195
+ messageId: row.messageId,
196
+ onAccepted: () => {
197
+ markReceptionOutboxSent(context.appHome, row.messageId);
198
+ },
199
+ },
200
+ );
201
+ } catch (error) {
202
+ markReceptionOutboxFailed(context.appHome, row.messageId, row.attemptCount, error);
203
+ }
204
+ }),
205
+ );
206
+ } finally {
207
+ draining = false;
208
+ }
209
+ };
210
+
211
+ return {
212
+ start: () => {
213
+ void connect();
214
+ refreshTimer = setInterval(() => {
215
+ if (!releaseLease) {
216
+ void connect();
217
+ return;
218
+ }
219
+ void refreshDirectory()
220
+ .then((roads) => {
221
+ if (roads.length && !session) void connect();
222
+ })
223
+ .catch((error) => debug(`directory refresh failed: ${(error as Error).message}`));
224
+ }, DIRECTORY_REFRESH_MS);
225
+ refreshTimer.unref();
226
+ outboxTimer = setInterval(() => void drainOutbox(), OUTBOX_INTERVAL_MS);
227
+ outboxTimer.unref();
228
+ },
229
+ close: () => {
230
+ stopped = true;
231
+ if (retryTimer) clearTimeout(retryTimer);
232
+ if (refreshTimer) clearInterval(refreshTimer);
233
+ if (outboxTimer) clearInterval(outboxTimer);
234
+ if (heartbeat) clearInterval(heartbeat);
235
+ retryTimer = null;
236
+ refreshTimer = null;
237
+ outboxTimer = null;
238
+ heartbeat = null;
239
+ try {
240
+ session?.close();
241
+ } catch {}
242
+ try {
243
+ socket?.close();
244
+ } catch {}
245
+ clearConnection();
246
+ releaseLease?.();
247
+ releaseLease = null;
248
+ },
249
+ };
250
+ }
251
+
252
+ function receptionEndpoint(ownerPrefix: string, deviceId: string): string {
253
+ return `${ownerPrefix}/rx-${deviceId.replaceAll('-', '').slice(0, 12)}`;
254
+ }
255
+
256
+ function receptionEnvelope(
257
+ deviceId: string,
258
+ endpoint: string,
259
+ message: UntrustedRoadText,
260
+ road: DeviceRoad | undefined,
261
+ ): BusEnvelope {
262
+ if (!road?.connectionId || road.kind !== 'connection') {
263
+ throw new Error('unknown_connection_road');
264
+ }
265
+ const person = decodePersonMessage(message.text);
266
+ return {
267
+ protocol: BUS_PROTOCOL,
268
+ id: `managed_${message.messageId.replaceAll('-', '')}`,
269
+ kind: 'road.message',
270
+ scope: 'road',
271
+ thread: null,
272
+ from: { city: message.from, actor: 'seat', role: 'external-seat' },
273
+ to: { city: endpoint, actor: 'seat' },
274
+ createdAt: new Date().toISOString(),
275
+ payload: {
276
+ text: person.text,
277
+ trust: 'information-not-authority',
278
+ transport: 'managed-e2ee',
279
+ messageKind: person.kind,
280
+ inReplyTo: person.inReplyTo,
281
+ sourceName: road.peerName,
282
+ connectionId: road.connectionId,
283
+ roadId: road.id,
284
+ remoteMessageId: message.messageId,
285
+ receiverDeviceId: deviceId,
286
+ },
287
+ };
288
+ }
289
+
290
+ function tryAcquireReceptionLease(appHome: string): (() => void) | null {
291
+ const lock = join(connectStateDirectory(appHome), 'reception.lock');
292
+ const owner = process.pid;
293
+ for (let attempt = 0; attempt < 2; attempt += 1) {
294
+ try {
295
+ const fd = openSync(lock, 'wx', 0o600);
296
+ try {
297
+ writeFileSync(fd, `${owner}\n`);
298
+ } finally {
299
+ closeSync(fd);
300
+ }
301
+ return () => {
302
+ if (lockOwner(lock) !== owner) return;
303
+ try {
304
+ unlinkSync(lock);
305
+ } catch {}
306
+ };
307
+ } catch (error) {
308
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
309
+ const info = lstatSync(lock);
310
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error('unsafe_reception_lock');
311
+ const old = lockOwner(lock);
312
+ if ((old > 0 && processAlive(old)) || (!old && Date.now() - statSync(lock).mtimeMs < 5_000)) {
313
+ return null;
314
+ }
315
+ try {
316
+ unlinkSync(lock);
317
+ } catch (unlinkError) {
318
+ if ((unlinkError as NodeJS.ErrnoException).code !== 'ENOENT') throw unlinkError;
319
+ }
320
+ }
321
+ }
322
+ return null;
323
+ }
324
+
325
+ function lockOwner(path: string): number {
326
+ if (!existsSync(path)) return 0;
327
+ try {
328
+ return Number(readFileSync(path, 'utf8').trim()) || 0;
329
+ } catch {
330
+ return 0;
331
+ }
332
+ }
333
+
334
+ function processAlive(pid: number): boolean {
335
+ try {
336
+ process.kill(pid, 0);
337
+ return true;
338
+ } catch {
339
+ return false;
340
+ }
341
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ ManagedRelaySession,
3
+ type RelaySessionOptions,
4
+ type RelayTransport,
5
+ type RoadInboxReceipt,
6
+ type UntrustedRoadText,
7
+ } from '../managed-connect-client.js';