agent-embassy 3.0.0 → 4.0.0

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 (115) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/CONTRIBUTING.md +92 -200
  3. package/README.md +159 -138
  4. package/SECURITY.md +191 -335
  5. package/dist/src/gateway/broker-check.d.ts +26 -0
  6. package/dist/src/gateway/broker-check.js +89 -0
  7. package/dist/src/gateway/broker-check.js.map +1 -0
  8. package/dist/src/gateway/broker-control.d.ts +72 -0
  9. package/dist/src/gateway/broker-control.js +164 -0
  10. package/dist/src/gateway/broker-control.js.map +1 -0
  11. package/dist/src/gateway/broker.d.ts +117 -0
  12. package/dist/src/gateway/broker.js +186 -0
  13. package/dist/src/gateway/broker.js.map +1 -0
  14. package/dist/src/gateway/claude-peer.d.ts +1 -124
  15. package/dist/src/gateway/claude-peer.js +62 -1053
  16. package/dist/src/gateway/claude-peer.js.map +1 -1
  17. package/dist/src/gateway/codex-stateless-transport.d.ts +0 -12
  18. package/dist/src/gateway/codex-stateless-transport.js +8 -142
  19. package/dist/src/gateway/codex-stateless-transport.js.map +1 -1
  20. package/dist/src/gateway/config.d.ts +2 -6
  21. package/dist/src/gateway/config.js +17 -27
  22. package/dist/src/gateway/config.js.map +1 -1
  23. package/dist/src/gateway/coordinator.d.ts +57 -0
  24. package/dist/src/gateway/coordinator.js +217 -0
  25. package/dist/src/gateway/coordinator.js.map +1 -0
  26. package/dist/src/gateway/core-cli.d.ts +15 -0
  27. package/dist/src/gateway/core-cli.js +282 -0
  28. package/dist/src/gateway/core-cli.js.map +1 -0
  29. package/dist/src/gateway/core-service-command.d.ts +16 -0
  30. package/dist/src/gateway/core-service-command.js +71 -0
  31. package/dist/src/gateway/core-service-command.js.map +1 -0
  32. package/dist/src/gateway/endpoint-directory.d.ts +33 -0
  33. package/dist/src/gateway/endpoint-directory.js +223 -0
  34. package/dist/src/gateway/endpoint-directory.js.map +1 -0
  35. package/dist/src/gateway/federation.d.ts +89 -0
  36. package/dist/src/gateway/federation.js +632 -0
  37. package/dist/src/gateway/federation.js.map +1 -0
  38. package/dist/src/gateway/ledger-codec.d.ts +3 -0
  39. package/dist/src/gateway/ledger-codec.js +159 -0
  40. package/dist/src/gateway/ledger-codec.js.map +1 -0
  41. package/dist/src/gateway/ledger.d.ts +138 -0
  42. package/dist/src/gateway/ledger.js +319 -0
  43. package/dist/src/gateway/ledger.js.map +1 -0
  44. package/dist/src/gateway/local-control.d.ts +23 -0
  45. package/dist/src/gateway/local-control.js +489 -0
  46. package/dist/src/gateway/local-control.js.map +1 -0
  47. package/dist/src/gateway/native-destinations.d.ts +34 -0
  48. package/dist/src/gateway/native-destinations.js +272 -0
  49. package/dist/src/gateway/native-destinations.js.map +1 -0
  50. package/dist/src/gateway/owned-state.d.ts +57 -0
  51. package/dist/src/gateway/owned-state.js +398 -0
  52. package/dist/src/gateway/owned-state.js.map +1 -0
  53. package/dist/src/gateway/provenance-envelope.d.ts +3 -3
  54. package/dist/src/gateway/provenance-envelope.js +3 -23
  55. package/dist/src/gateway/provenance-envelope.js.map +1 -1
  56. package/dist/src/gateway/runtime.d.ts +41 -0
  57. package/dist/src/gateway/runtime.js +175 -0
  58. package/dist/src/gateway/runtime.js.map +1 -0
  59. package/docs/CONFIGURATION.md +178 -266
  60. package/docs/DELIVERY.md +133 -82
  61. package/docs/GATEWAY-ARCHITECTURE.md +276 -943
  62. package/package.json +5 -5
  63. package/skills/embassy-peer/SKILL.md +38 -194
  64. package/skills/embassy-peer/agents/openai.yaml +2 -2
  65. package/dist/src/gateway/claude-helper-client.d.ts +0 -2
  66. package/dist/src/gateway/claude-helper-client.js +0 -2
  67. package/dist/src/gateway/claude-helper-client.js.map +0 -1
  68. package/dist/src/gateway/claude-helper-protocol.d.ts +0 -117
  69. package/dist/src/gateway/claude-helper-protocol.js +0 -122
  70. package/dist/src/gateway/claude-helper-protocol.js.map +0 -1
  71. package/dist/src/gateway/claude-helper-supervisor.d.ts +0 -87
  72. package/dist/src/gateway/claude-helper-supervisor.js +0 -347
  73. package/dist/src/gateway/claude-helper-supervisor.js.map +0 -1
  74. package/dist/src/gateway/claude-helper.d.ts +0 -1
  75. package/dist/src/gateway/claude-helper.js +0 -252
  76. package/dist/src/gateway/claude-helper.js.map +0 -1
  77. package/dist/src/gateway/cli.d.ts +0 -57
  78. package/dist/src/gateway/cli.js +0 -1503
  79. package/dist/src/gateway/cli.js.map +0 -1
  80. package/dist/src/gateway/codex-socket-holder.d.ts +0 -26
  81. package/dist/src/gateway/codex-socket-holder.js +0 -76
  82. package/dist/src/gateway/codex-socket-holder.js.map +0 -1
  83. package/dist/src/gateway/control.d.ts +0 -261
  84. package/dist/src/gateway/control.js +0 -903
  85. package/dist/src/gateway/control.js.map +0 -1
  86. package/dist/src/gateway/peer-client.d.ts +0 -58
  87. package/dist/src/gateway/peer-client.js +0 -217
  88. package/dist/src/gateway/peer-client.js.map +0 -1
  89. package/dist/src/gateway/peer-mailbox.d.ts +0 -78
  90. package/dist/src/gateway/peer-mailbox.js +0 -168
  91. package/dist/src/gateway/peer-mailbox.js.map +0 -1
  92. package/dist/src/gateway/peer-protocol.d.ts +0 -105
  93. package/dist/src/gateway/peer-protocol.js +0 -70
  94. package/dist/src/gateway/peer-protocol.js.map +0 -1
  95. package/dist/src/gateway/peer-stdio.d.ts +0 -23
  96. package/dist/src/gateway/peer-stdio.js +0 -100
  97. package/dist/src/gateway/peer-stdio.js.map +0 -1
  98. package/dist/src/gateway/providers.d.ts +0 -163
  99. package/dist/src/gateway/providers.js +0 -972
  100. package/dist/src/gateway/providers.js.map +0 -1
  101. package/dist/src/gateway/server.d.ts +0 -47
  102. package/dist/src/gateway/server.js +0 -219
  103. package/dist/src/gateway/server.js.map +0 -1
  104. package/dist/src/gateway/service.d.ts +0 -373
  105. package/dist/src/gateway/service.js +0 -2274
  106. package/dist/src/gateway/service.js.map +0 -1
  107. package/dist/src/gateway/status-view.d.ts +0 -167
  108. package/dist/src/gateway/status-view.js +0 -488
  109. package/dist/src/gateway/status-view.js.map +0 -1
  110. package/dist/src/gateway/store.d.ts +0 -185
  111. package/dist/src/gateway/store.js +0 -2359
  112. package/dist/src/gateway/store.js.map +0 -1
  113. package/dist/src/gateway/types.d.ts +0 -528
  114. package/dist/src/gateway/types.js +0 -341
  115. package/dist/src/gateway/types.js.map +0 -1
@@ -1,972 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import path from "node:path";
3
- import { BridgeError } from "../errors.js";
4
- import { isAttestedGatewayNodeInventory } from "./federation-nodes.js";
5
- import { CLAUDE_PEER_COMPATIBILITY, ClaudePeerAdapter, } from "./claude-peer.js";
6
- import { ClaudeNativeHelperSupervisor, } from "./claude-helper-supervisor.js";
7
- import { composeProvenanceEnvelope } from "./provenance-envelope.js";
8
- const NATIVE_CLAUDE_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
9
- const PUBLIC_ALIAS = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
10
- const OPAQUE_ROUTE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
11
- const MAX_CLAUDE_PENDING = 1_024;
12
- const DEFAULT_CLAUDE_DISCOVERY_POLL_MS = 1_000;
13
- const DEFAULT_CODEX_OBSERVATION_POLL_MS = 15_000;
14
- const DEFAULT_CODEX_OBSERVATION_TIMEOUT_MS = 15_000;
15
- const DEFAULT_CODEX_OBSERVATION_BACKOFF_MAX_MS = 60_000;
16
- const MAX_CODEX_OBSERVED_ROUTES = 128;
17
- const CLAUDE_CLEAN_PREWRITE_RETRY_CODES = new Set([
18
- "CLAUDE_PEER_TARGET_UNKNOWN",
19
- "CLAUDE_PEER_TARGET_STALE",
20
- "CLAUDE_PEER_TARGET_CHANGED",
21
- "CLAUDE_PEER_WORKSPACE_UNATTESTED",
22
- ]);
23
- const CODEX_CLEAN_RETRY_CODES = new Set([
24
- "THREAD_NOT_OBSERVED",
25
- "ROUTE_BUSY",
26
- "APPROVAL_REQUIRED",
27
- "MANAGED_CODEX_UNAVAILABLE",
28
- "LOCAL_APP_SERVER_NOT_RUNNING",
29
- "ENDPOINT_GENERATION_CHANGED",
30
- "SPAWN_FAILED",
31
- "SPAWN_TIMEOUT",
32
- "TRANSPORT_CONNECT_FAILED",
33
- "REQUEST_TIMEOUT",
34
- ]);
35
- function claudeCleanPrewriteResult(error) {
36
- if (error instanceof BridgeError &&
37
- error.code === "CLAUDE_PEER_MESSAGE_EXPIRED") {
38
- return { state: "expired", safeErrorCode: "MESSAGE_EXPIRED" };
39
- }
40
- if (error instanceof BridgeError &&
41
- CLAUDE_CLEAN_PREWRITE_RETRY_CODES.has(error.code)) {
42
- return { state: "deferred", safeErrorCode: "ROUTE_BUSY" };
43
- }
44
- const systemCode = typeof error === "object" &&
45
- error !== null &&
46
- "code" in error &&
47
- typeof error.code === "string"
48
- ? error.code
49
- : undefined;
50
- return {
51
- state: "failed",
52
- safeErrorCode: error instanceof BridgeError
53
- ? error.code
54
- : systemCode === "ENOENT"
55
- ? "CLAUDE_DISPATCH_PREWRITE_PATH_MISSING"
56
- : systemCode === "EACCES" || systemCode === "EPERM"
57
- ? "CLAUDE_DISPATCH_PREWRITE_ACCESS_DENIED"
58
- : systemCode === "ETIMEDOUT"
59
- ? "CLAUDE_DISPATCH_PREWRITE_TIMEOUT"
60
- : "CLAUDE_DISPATCH_PREWRITE_FAILED",
61
- };
62
- }
63
- function claudePostAuthorizationResult(error) {
64
- if (error instanceof BridgeError && error.recoverable) {
65
- return {
66
- state: "failed",
67
- safeErrorCode: error.code === "CLAUDE_PEER_MESSAGE_EXPIRED"
68
- ? "MESSAGE_EXPIRED"
69
- : error.code,
70
- };
71
- }
72
- return {
73
- state: "ambiguous",
74
- safeErrorCode: error instanceof BridgeError &&
75
- /^[A-Z][A-Z0-9_]{0,95}$/.test(error.code)
76
- ? error.code
77
- : "CLAUDE_DISPATCH_OUTCOME_AMBIGUOUS",
78
- };
79
- }
80
- async function cancelPreparedClaude(prepared) {
81
- try {
82
- await prepared.cancel();
83
- }
84
- catch {
85
- // Cancellation is a no-write cleanup path. Its failure cannot turn a
86
- // denied or uncertain authorization into permission to perform.
87
- }
88
- }
89
- function exactLocalHost(hostId, inventory) {
90
- if (!isAttestedGatewayNodeInventory(inventory, hostId)) {
91
- throw new BridgeError("GATEWAY_REMOTE_PROVIDER_DISABLED", "The local host coordinate requires an attested node inventory.");
92
- }
93
- return hostId;
94
- }
95
- function positiveBounded(value, fallback, maximum) {
96
- const resolved = value ?? fallback;
97
- if (!Number.isSafeInteger(resolved) ||
98
- resolved <= 0 ||
99
- resolved > maximum) {
100
- throw new BridgeError("INVALID_GATEWAY_PROVIDER_CONFIGURATION", "A provider capacity is outside its reviewed bound.");
101
- }
102
- return resolved;
103
- }
104
- function sameEndpoint(binding, identity) {
105
- return (binding.provider === identity.provider &&
106
- binding.hostId === identity.hostId);
107
- }
108
- function invokeCallback(operation) {
109
- try {
110
- operation();
111
- }
112
- catch {
113
- // Provider observation consumers cannot change a transport outcome.
114
- }
115
- }
116
- function validateClaudeRuntimePathEvidence(runtime) {
117
- if ([runtime.sessionsDir, runtime.socketDir].some((value) => !path.isAbsolute(value) ||
118
- path.normalize(value) !== value ||
119
- value.includes("\0"))) {
120
- throw new BridgeError("CLAUDE_RUNTIME_ATTESTATION_INVALID", "The local Claude provider requires bounded absolute runtime evidence.");
121
- }
122
- }
123
- function claudeRouteState(peer) {
124
- return peer.status === "idle" ? "idle" : "busy";
125
- }
126
- function claudeRegistryObservation(discovery) {
127
- const rejected = Object.entries(discovery.rejected)
128
- .filter((entry) => Number.isSafeInteger(entry[1]) && entry[1] > 0)
129
- .sort(([left], [right]) => left.localeCompare(right))
130
- .map(([safeErrorCode, count]) => Object.freeze({ safeErrorCode, count }));
131
- return Object.freeze({
132
- entriesScanned: discovery.entriesScanned,
133
- parseableRecords: discovery.parseableRecords,
134
- rejected: Object.freeze(rejected),
135
- });
136
- }
137
- function unavailableClaudeRegistryObservation() {
138
- return Object.freeze({
139
- entriesScanned: 0,
140
- parseableRecords: 0,
141
- rejected: Object.freeze([
142
- Object.freeze({
143
- safeErrorCode: "CLAUDE_REGISTRY_UNAVAILABLE",
144
- count: 1,
145
- }),
146
- ]),
147
- });
148
- }
149
- export class LocalClaudeGatewayProvider {
150
- identity;
151
- protocol = "claude-peer";
152
- protocolVersion = `${CLAUDE_PEER_COMPATIBILITY.peerProtocol}`;
153
- peer;
154
- stateRoot;
155
- maxPending;
156
- discoveryPollMs;
157
- now;
158
- nativeHelpers;
159
- discovered = new Map();
160
- /** Exact UUID selection plus its display-only observation bookkeeping. */
161
- selected = new Map();
162
- discoveryInFlight;
163
- registryObservation;
164
- monitorTimer;
165
- callbacks;
166
- initialized = false;
167
- closed = false;
168
- constructor(options) {
169
- validateClaudeRuntimePathEvidence(options.runtime);
170
- if (!path.isAbsolute(options.stateRoot) || options.stateRoot.includes("\0")) {
171
- throw new BridgeError("INVALID_GATEWAY_PROVIDER_CONFIGURATION", "The Claude provider state root must be an absolute path.");
172
- }
173
- const hostId = exactLocalHost(options.hostId, options.nodeInventory);
174
- this.identity = {
175
- provider: "claude",
176
- hostId,
177
- };
178
- this.stateRoot = options.stateRoot;
179
- this.maxPending = positiveBounded(options.maxPendingMessages, MAX_CLAUDE_PENDING, 4_096);
180
- this.discoveryPollMs = positiveBounded(options.discoveryPollMs, DEFAULT_CLAUDE_DISCOVERY_POLL_MS, 30_000);
181
- this.now = options.now ?? Date.now;
182
- const protocolRuntime = options.runtime;
183
- this.peer = (options.peerFactory ??
184
- ((runtime, deliveryNotices) => new ClaudePeerAdapter({
185
- sessionsDir: runtime.sessionsDir,
186
- socketDir: runtime.socketDir,
187
- deliveryNotices,
188
- })))(protocolRuntime, options.deliveryNotices ?? "merged");
189
- this.nativeHelpers =
190
- options.nativeHelpers === undefined
191
- ? undefined
192
- : new ClaudeNativeHelperSupervisor({
193
- identity: this.identity,
194
- runtime: protocolRuntime,
195
- deliveryNotices: options.deliveryNotices ?? "merged",
196
- maxPendingMessages: this.maxPending,
197
- maxHelpers: positiveBounded(options.nativeHelpers.maxHelpers, 256, 128),
198
- callbacks: () => this.callbacks,
199
- ...(options.nativeHelpers.factory === undefined
200
- ? {}
201
- : { factory: options.nativeHelpers.factory }),
202
- });
203
- }
204
- latestRegistryObservation() {
205
- return this.registryObservation;
206
- }
207
- async initialize(callbacks) {
208
- if (this.closed) {
209
- throw new BridgeError("CLAUDE_PROVIDER_CLOSED", "The local Claude provider is closed.");
210
- }
211
- if (this.initialized) {
212
- throw new BridgeError("CLAUDE_PROVIDER_ALREADY_INITIALIZED", "The local Claude provider can be initialized only once.");
213
- }
214
- this.callbacks = callbacks;
215
- this.initialized = true;
216
- return { health: "healthy" };
217
- }
218
- async discoverClaudePeers() {
219
- this.assertReady();
220
- return await this.refreshClaudeDiscovery();
221
- }
222
- async assertWorkspaceDisjoint(routeHandle, stateRoot) {
223
- this.assertReady();
224
- await this.peer.assertTargetWorkspaceDisjoint(routeHandle, stateRoot);
225
- const route = this.selected.get(routeHandle);
226
- const alias = route?.alias ?? this.discovered.get(routeHandle)?.alias;
227
- if (alias !== undefined) {
228
- this.selected.set(routeHandle, {
229
- alias,
230
- dispatchEpoch: route?.dispatchEpoch ?? 0,
231
- observationDirty: route?.observationDirty ?? false,
232
- ...(route?.registrationId === undefined ? {} : { registrationId: route.registrationId }),
233
- ...(route?.observation === undefined ? {} : { observation: route.observation }),
234
- stateRoot,
235
- });
236
- }
237
- }
238
- async selectRoute(input) {
239
- this.assertReady();
240
- const candidate = this.discovered.get(input.routeHandle);
241
- if (candidate === undefined || candidate.alias !== input.alias) {
242
- // The TARGET moved between discovery and this pin — it renamed or left.
243
- // A distinct code from the caller-side CLAUDE_ROUTE_MISMATCH, so the CLI
244
- // cannot answer "fix your --from" to a problem at the other end.
245
- throw new BridgeError("CLAUDE_TARGET_CHANGED", "The addressed Claude session no longer matches that exact peer generation.");
246
- }
247
- const route = this.selected.get(input.routeHandle);
248
- this.selected.set(input.routeHandle, {
249
- alias: input.alias,
250
- dispatchEpoch: route?.dispatchEpoch ?? 0,
251
- observation: `${candidate.state}:`,
252
- observationDirty: false,
253
- ...(route?.registrationId === undefined ? {} : { registrationId: route.registrationId }),
254
- ...(route?.stateRoot === undefined ? {} : { stateRoot: route.stateRoot }),
255
- });
256
- this.scheduleClaudeMonitor();
257
- return { routeHandle: input.routeHandle, state: candidate.state };
258
- }
259
- observeLogicalRoute(input) {
260
- if (this.closed ||
261
- !PUBLIC_ALIAS.test(input.alias) ||
262
- !input.alias.endsWith(`@${this.identity.hostId}`) ||
263
- !OPAQUE_ROUTE.test(input.routeHandle) ||
264
- !OPAQUE_ROUTE.test(input.registrationId)) {
265
- return;
266
- }
267
- const route = this.selected.get(input.routeHandle);
268
- this.selected.set(input.routeHandle, {
269
- alias: input.alias,
270
- registrationId: input.registrationId,
271
- stateRoot: this.stateRoot,
272
- dispatchEpoch: route?.dispatchEpoch ?? 0,
273
- observationDirty: route?.observationDirty ?? false,
274
- ...(route?.observation === undefined ? {} : { observation: route.observation }),
275
- });
276
- }
277
- forgetLogicalRoute(registrationId) {
278
- for (const [routeHandle, route] of this.selected) {
279
- if (route.registrationId === registrationId) {
280
- this.selected.delete(routeHandle);
281
- }
282
- }
283
- }
284
- /**
285
- * Converts an inherited messaging-socket capability into the owning
286
- * session's UUID. The session need not hold a route yet: its route installs
287
- * on its first send, and the service compares the claimed alias against
288
- * discovery for that UUID.
289
- */
290
- async resolveReplyAddress(address) {
291
- this.assertReady();
292
- const resolved = await this.peer.resolveReplyAddress(address);
293
- if (resolved.kind !== "interactive" && resolved.kind !== "bg") {
294
- throw new BridgeError("CLAUDE_REPLY_ROUTE_MISMATCH", "The reply capability does not belong to a Claude session.");
295
- }
296
- return { routeHandle: resolved.targetId };
297
- }
298
- async advertiseNativeSourcePeer(input) {
299
- await this.helpers().advertise(input);
300
- }
301
- async unadvertiseNativeSourcePeer(alias) {
302
- await this.helpers().unadvertise(alias);
303
- }
304
- async updateNativeSourcePeerStatus(alias, status) {
305
- await this.helpers().updateStatus(alias, status);
306
- }
307
- async updateNativeInboundStatus(receiptHandle, status, diagnosticCode) {
308
- await this.helpers().updateInboundStatus(receiptHandle, status, diagnosticCode);
309
- }
310
- async notifyNativeInboundProgress(receiptHandle, progress) {
311
- await this.helpers().notifyInboundProgress(receiptHandle, progress);
312
- }
313
- async releaseNativeInboundReceipt(receiptHandle) {
314
- return await this.helpers().releaseInboundReceipt(receiptHandle);
315
- }
316
- async dispatch(input) {
317
- if (!this.initialized ||
318
- this.closed ||
319
- !sameEndpoint(input.binding, this.identity)) {
320
- return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
321
- }
322
- if (input.authorization === "selected_route") {
323
- const logical = this.selected.get(input.binding.routeHandle);
324
- if (logical === undefined ||
325
- logical.alias !== input.targetAlias ||
326
- logical.registrationId !== input.binding.registrationId) {
327
- return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
328
- }
329
- logical.dispatchEpoch += 1;
330
- logical.observationDirty = true;
331
- this.emitClaudeRouteObservation(input.binding.routeHandle, "busy");
332
- }
333
- let prepared;
334
- try {
335
- const selected = this.selected.get(input.binding.routeHandle);
336
- prepared = await this.helpers().prepareDispatch({
337
- sourceAlias: input.sourceAlias,
338
- sourceProvider: input.sourceProvider,
339
- targetAlias: input.targetAlias,
340
- conversationId: input.conversationId,
341
- binding: input.binding,
342
- authorization: input.authorization,
343
- messageId: input.messageId,
344
- text: input.text,
345
- expectsReply: input.expectsReply,
346
- deadlineAt: input.deadlineAt,
347
- ...(selected?.alias === undefined ? {} : { selectedAlias: selected.alias }),
348
- ...(input.authorization !== "selected_route" || selected?.stateRoot === undefined
349
- ? {}
350
- : { stateRoot: selected.stateRoot }),
351
- });
352
- }
353
- catch (error) {
354
- if (error instanceof BridgeError &&
355
- CLAUDE_CLEAN_PREWRITE_RETRY_CODES.has(error.code)) {
356
- this.emitClaudeRouteObservation(input.binding.routeHandle, "busy", error.code);
357
- }
358
- return claudeCleanPrewriteResult(error);
359
- }
360
- const evidence = {
361
- attemptId: input.attemptId,
362
- kind: "claude_mailbox",
363
- bodyBytes: Buffer.byteLength(input.text, "utf8"),
364
- bodySha256: createHash("sha256").update(input.text).digest("hex"),
365
- frameBytes: prepared.frameBytes,
366
- sha256: prepared.sha256,
367
- };
368
- let authorized;
369
- try {
370
- authorized = await input.authorizeWrite(evidence);
371
- }
372
- catch {
373
- await cancelPreparedClaude(prepared);
374
- return {
375
- state: "ambiguous",
376
- safeErrorCode: "WRITE_AUTHORIZATION_UNCERTAIN",
377
- };
378
- }
379
- if (!authorized) {
380
- await cancelPreparedClaude(prepared);
381
- return {
382
- state: "failed",
383
- safeErrorCode: "WRITE_AUTHORIZATION_DENIED",
384
- };
385
- }
386
- try {
387
- // Authorization is the write-authorization linearization point. Invoke the exact
388
- // prepared mailbox write in the same continuation with no intervening
389
- // await. Claude has no separate acceptance phase: confirmed mailbox
390
- // completion is terminal delivered.
391
- const operation = prepared.perform();
392
- const result = await operation;
393
- if (result.state === "deferred") {
394
- return {
395
- state: "ambiguous",
396
- safeErrorCode: "CLAUDE_PREPARED_RESULT_INVALID",
397
- };
398
- }
399
- return result.state === "expired"
400
- ? { state: "failed", safeErrorCode: "MESSAGE_EXPIRED" }
401
- : result;
402
- }
403
- catch (error) {
404
- return claudePostAuthorizationResult(error);
405
- }
406
- }
407
- async releaseRoute(routeHandle) {
408
- this.selected.delete(routeHandle);
409
- this.discovered.delete(routeHandle);
410
- if (this.selected.size === 0 && this.monitorTimer !== undefined) {
411
- clearTimeout(this.monitorTimer);
412
- this.monitorTimer = undefined;
413
- }
414
- }
415
- async close() {
416
- if (this.closed)
417
- return;
418
- this.closed = true;
419
- try {
420
- await this.nativeHelpers?.close();
421
- await this.peer.close();
422
- }
423
- finally {
424
- this.selected.clear();
425
- this.discovered.clear();
426
- if (this.monitorTimer !== undefined)
427
- clearTimeout(this.monitorTimer);
428
- this.monitorTimer = undefined;
429
- this.callbacks = undefined;
430
- }
431
- }
432
- assertReady() {
433
- if (!this.initialized || this.closed) {
434
- throw new BridgeError("CLAUDE_PROVIDER_UNAVAILABLE", "The local Claude provider is not available.", true);
435
- }
436
- }
437
- helpers() {
438
- this.assertReady();
439
- if (this.nativeHelpers === undefined) {
440
- throw new BridgeError("CLAUDE_NATIVE_HELPER_UNAVAILABLE", "The supervised native Claude helper is unavailable.", true);
441
- }
442
- return this.nativeHelpers;
443
- }
444
- async refreshClaudeDiscovery() {
445
- if (this.discoveryInFlight !== undefined) {
446
- return await this.discoveryInFlight;
447
- }
448
- const operation = this.refreshClaudeDiscoveryOnce();
449
- this.discoveryInFlight = operation;
450
- try {
451
- return await operation;
452
- }
453
- finally {
454
- if (this.discoveryInFlight === operation) {
455
- this.discoveryInFlight = undefined;
456
- }
457
- }
458
- }
459
- async refreshClaudeDiscoveryOnce() {
460
- const dispatchEpochAtStart = new Map();
461
- for (const [routeHandle, route] of this.selected) {
462
- dispatchEpochAtStart.set(routeHandle, route.dispatchEpoch);
463
- }
464
- let discovery;
465
- try {
466
- discovery = await this.peer.discover();
467
- }
468
- catch (error) {
469
- this.registryObservation = unavailableClaudeRegistryObservation();
470
- throw error;
471
- }
472
- const registry = claudeRegistryObservation(discovery);
473
- this.registryObservation = registry;
474
- this.discovered.clear();
475
- const rows = [];
476
- for (const peer of discovery.peers) {
477
- if ((peer.kind !== "interactive" && peer.kind !== "bg") ||
478
- !NATIVE_CLAUDE_NAME.test(peer.alias)) {
479
- continue;
480
- }
481
- const alias = `${peer.alias}@${this.identity.hostId}`;
482
- if (!PUBLIC_ALIAS.test(alias))
483
- continue;
484
- const row = {
485
- alias,
486
- routeHandle: peer.targetId,
487
- kind: peer.kind,
488
- state: claudeRouteState(peer),
489
- };
490
- this.discovered.set(peer.targetId, row);
491
- rows.push(row);
492
- }
493
- for (const [routeHandle, selected] of this.selected) {
494
- const row = this.discovered.get(routeHandle);
495
- if (row === undefined) {
496
- this.emitClaudeRouteObservation(routeHandle, "busy", "CLAUDE_PEER_NOT_OBSERVED", true);
497
- }
498
- else {
499
- if (!discovery.truncated && row.alias !== selected.alias) {
500
- // A Claude rename changes only the live name index. The native
501
- // session UUID remains the selected logical route. An incomplete
502
- // scan is display evidence only and cannot rename dispatch authority.
503
- selected.alias = row.alias;
504
- }
505
- if (row.state === "idle" &&
506
- (dispatchEpochAtStart.get(routeHandle) !==
507
- selected.dispatchEpoch)) {
508
- // Never publish an idle sample from a discovery that overlapped the
509
- // exact mailbox write. A wholly post-terminal discovery may clear
510
- // the dirty epoch.
511
- continue;
512
- }
513
- this.emitClaudeRouteObservation(routeHandle, row.state, undefined, true);
514
- }
515
- }
516
- return { peers: rows, complete: !discovery.truncated, registry };
517
- }
518
- emitClaudeRouteObservation(routeHandle, state, safeErrorCode, authoritative = false) {
519
- const signature = `${state}:${safeErrorCode ?? ""}`;
520
- const selected = this.selected.get(routeHandle);
521
- const mustPublish = authoritative && selected?.observationDirty === true;
522
- if (selected !== undefined && mustPublish)
523
- selected.observationDirty = false;
524
- if (!mustPublish && selected?.observation === signature) {
525
- return;
526
- }
527
- if (selected !== undefined)
528
- selected.observation = signature;
529
- const callbacks = this.callbacks;
530
- const registrationId = selected?.registrationId;
531
- if (callbacks === undefined ||
532
- this.closed ||
533
- registrationId === undefined) {
534
- return;
535
- }
536
- invokeCallback(() => callbacks.onRouteState({
537
- route: {
538
- ...this.identity,
539
- routeHandle,
540
- registrationId,
541
- },
542
- observedAt: new Date(this.now()).toISOString(),
543
- state,
544
- ...(safeErrorCode === undefined ? {} : { safeErrorCode }),
545
- }));
546
- }
547
- scheduleClaudeMonitor() {
548
- if (this.closed ||
549
- this.selected.size === 0 ||
550
- this.monitorTimer !== undefined) {
551
- return;
552
- }
553
- this.monitorTimer = setTimeout(() => {
554
- this.monitorTimer = undefined;
555
- void this.refreshClaudeDiscovery()
556
- .catch(() => {
557
- for (const routeHandle of this.selected.keys()) {
558
- this.emitClaudeRouteObservation(routeHandle, "busy", "CLAUDE_DISCOVERY_UNAVAILABLE");
559
- }
560
- })
561
- .finally(() => this.scheduleClaudeMonitor());
562
- }, this.discoveryPollMs);
563
- this.monitorTimer.unref();
564
- }
565
- }
566
- export function createLocalClaudeGatewayProvider(options) {
567
- return new LocalClaudeGatewayProvider(options);
568
- }
569
- function boundedProviderOption(value, fallback, maximum) {
570
- const candidate = value ?? fallback;
571
- if (!Number.isSafeInteger(candidate) ||
572
- candidate <= 0 ||
573
- candidate > maximum) {
574
- throw new BridgeError("INVALID_GATEWAY_PROVIDER_CONFIGURATION", "The local Codex provider has an invalid bounded option.");
575
- }
576
- return candidate;
577
- }
578
- function cleanCodexResult(result) {
579
- if (result.safeErrorCode === "MESSAGE_EXPIRED") {
580
- return { state: "expired", safeErrorCode: "MESSAGE_EXPIRED" };
581
- }
582
- if (CODEX_CLEAN_RETRY_CODES.has(result.safeErrorCode)) {
583
- return { state: "deferred", safeErrorCode: result.safeErrorCode };
584
- }
585
- return { state: "failed", safeErrorCode: result.safeErrorCode };
586
- }
587
- function terminalCodexResult(result, expectsReply) {
588
- if (result.outcome === "failed") {
589
- return { state: "failed", safeErrorCode: "CODEX_TURN_FAILED" };
590
- }
591
- if (result.outcome === "interrupted") {
592
- return { state: "cancelled", safeErrorCode: "CODEX_TURN_INTERRUPTED" };
593
- }
594
- return {
595
- state: "delivered",
596
- ...(result.replyCode === "REPLY_TOO_LARGE"
597
- ? { safeErrorCode: "CODEX_REPLY_TOO_LARGE" }
598
- : {}),
599
- ...(expectsReply && result.replyText !== null
600
- ? { replyText: result.replyText }
601
- : {}),
602
- };
603
- }
604
- function mapCodexStartResult(result, expectsReply) {
605
- if (result.phase === "clean")
606
- return cleanCodexResult(result);
607
- if (result.phase === "armed") {
608
- return { state: "ambiguous", safeErrorCode: result.safeErrorCode };
609
- }
610
- if (result.phase === "accepted") {
611
- return { state: "unconfirmed", safeErrorCode: result.safeErrorCode };
612
- }
613
- return terminalCodexResult(result, expectsReply);
614
- }
615
- function mapCodexSteerResult(result) {
616
- if (result.phase === "clean")
617
- return cleanCodexResult(result);
618
- if (result.phase === "armed") {
619
- return { state: "ambiguous", safeErrorCode: result.safeErrorCode };
620
- }
621
- return { state: "delivered" };
622
- }
623
- /**
624
- * Thin logical Codex adapter. Registration and construction perform no App
625
- * Server I/O. Each semantic operation owns a fresh attested transport, while
626
- * the independent observer publishes only best-effort in-memory freshness.
627
- */
628
- export class LocalCodexGatewayProvider {
629
- hostId;
630
- operation;
631
- observationPollMs;
632
- observationTimeoutMs;
633
- observationBackoffMaxMs;
634
- maxObservedRoutes;
635
- now;
636
- timers;
637
- observedRoutes = new Map();
638
- activeTurns = new Map();
639
- inFlightOperations = new Set();
640
- operationControllers = new Set();
641
- callbacks;
642
- observationTimer;
643
- observationInFlight;
644
- observationController;
645
- initialized = false;
646
- closing = false;
647
- closed = false;
648
- constructor(options) {
649
- if (!isAttestedGatewayNodeInventory(options.nodeInventory, options.hostId) ||
650
- options.operation === undefined ||
651
- typeof options.operation.execute !== "function") {
652
- throw new BridgeError("INVALID_GATEWAY_PROVIDER_CONFIGURATION", "The stateless Codex provider requires the exact attested local host.");
653
- }
654
- this.hostId = options.hostId;
655
- this.operation = options.operation;
656
- this.observationPollMs = boundedProviderOption(options.observationPollMs, DEFAULT_CODEX_OBSERVATION_POLL_MS, 60_000);
657
- this.observationTimeoutMs = boundedProviderOption(options.observationTimeoutMs, DEFAULT_CODEX_OBSERVATION_TIMEOUT_MS, 60_000);
658
- this.observationBackoffMaxMs = boundedProviderOption(options.observationBackoffMaxMs, DEFAULT_CODEX_OBSERVATION_BACKOFF_MAX_MS, 5 * 60_000);
659
- if (this.observationPollMs > this.observationBackoffMaxMs) {
660
- throw new BridgeError("INVALID_GATEWAY_PROVIDER_CONFIGURATION", "Codex observer backoff must cover its polling interval.");
661
- }
662
- this.maxObservedRoutes = boundedProviderOption(options.maxObservedRoutes, MAX_CODEX_OBSERVED_ROUTES, 256);
663
- this.now = options.now ?? (() => new Date());
664
- this.timers = options.timers ?? {
665
- setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
666
- clearTimeout: (timer) => clearTimeout(timer),
667
- };
668
- }
669
- get identity() {
670
- return { provider: "codex", hostId: this.hostId };
671
- }
672
- get protocol() {
673
- return "codex-app-server";
674
- }
675
- get protocolVersion() {
676
- return "stateless-v1";
677
- }
678
- async initialize(callbacks) {
679
- if (this.initialized || this.closing || this.closed) {
680
- throw new BridgeError("CODEX_PROVIDER_INITIALIZATION_REJECTED", "The stateless Codex provider cannot initialize in this state.");
681
- }
682
- this.callbacks = callbacks;
683
- this.initialized = true;
684
- this.scheduleObservation();
685
- return { health: "healthy" };
686
- }
687
- observeLogicalRoute(input) {
688
- if (!this.initialized ||
689
- this.closing ||
690
- this.closed ||
691
- !PUBLIC_ALIAS.test(input.alias) ||
692
- !input.alias.endsWith("@" + this.hostId) ||
693
- !OPAQUE_ROUTE.test(input.routeHandle) ||
694
- !OPAQUE_ROUTE.test(input.registrationId)) {
695
- return;
696
- }
697
- const existing = this.observedRoutes.get(input.registrationId);
698
- if (existing === undefined &&
699
- this.observedRoutes.size >= this.maxObservedRoutes) {
700
- return;
701
- }
702
- this.observedRoutes.set(input.registrationId, {
703
- ...input,
704
- attempt: 0,
705
- nextAt: this.now().getTime(),
706
- });
707
- this.scheduleObservation();
708
- }
709
- forgetLogicalRoute(registrationId) {
710
- this.observedRoutes.delete(registrationId);
711
- this.activeTurns.delete(registrationId);
712
- this.scheduleObservation();
713
- }
714
- async dispatch(input) {
715
- if (!this.initialized ||
716
- this.closing ||
717
- this.closed ||
718
- input.authorization !== "selected_route" ||
719
- input.binding.provider !== "codex" ||
720
- input.binding.hostId !== this.hostId ||
721
- !PUBLIC_ALIAS.test(input.targetAlias) ||
722
- !input.targetAlias.endsWith("@" + this.hostId)) {
723
- return { state: "failed", safeErrorCode: "CODEX_ROUTE_UNAVAILABLE" };
724
- }
725
- let content;
726
- try {
727
- content = composeProvenanceEnvelope({
728
- sourceProvider: input.sourceProvider,
729
- recipientProvider: "codex",
730
- sourceAlias: input.sourceAlias,
731
- targetAlias: input.targetAlias,
732
- conversationId: input.conversationId,
733
- body: input.text,
734
- ...(input.steer === true && input.queuedAhead !== undefined
735
- ? { queuedAhead: input.queuedAhead }
736
- : {}),
737
- });
738
- }
739
- catch (error) {
740
- return {
741
- state: "failed",
742
- safeErrorCode: error instanceof BridgeError &&
743
- error.code === "PROVENANCE_ENVELOPE_TOO_LARGE"
744
- ? error.code
745
- : "PROVENANCE_ENVELOPE_INVALID",
746
- };
747
- }
748
- if (input.steer === true) {
749
- const active = this.activeTurns.get(input.binding.registrationId);
750
- if (active === undefined ||
751
- active.alias !== input.targetAlias ||
752
- active.routeHandle !== input.binding.routeHandle) {
753
- return { state: "deferred", safeErrorCode: "ROUTE_BUSY" };
754
- }
755
- const steering = active.accepted.steer({
756
- attemptId: input.attemptId,
757
- authorizeWrite: async (evidence) => await input.authorizeWrite({
758
- attemptId: input.attemptId,
759
- kind: "codex_turn_steer",
760
- bodyBytes: Buffer.byteLength(input.text, "utf8"),
761
- bodySha256: createHash("sha256").update(input.text).digest("hex"),
762
- frameBytes: evidence.frameBytes,
763
- sha256: evidence.sha256,
764
- }),
765
- deadlineAt: input.deadlineAt,
766
- text: content,
767
- });
768
- this.inFlightOperations.add(steering);
769
- try {
770
- return mapCodexSteerResult(await steering);
771
- }
772
- finally {
773
- this.inFlightOperations.delete(steering);
774
- }
775
- }
776
- if (this.activeTurns.has(input.binding.registrationId)) {
777
- return { state: "deferred", safeErrorCode: "ROUTE_BUSY" };
778
- }
779
- let accepted;
780
- const controller = new AbortController();
781
- this.operationControllers.add(controller);
782
- const operation = this.operation.execute({
783
- attemptId: input.attemptId,
784
- authorizeWrite: async (evidence) => await input.authorizeWrite({
785
- attemptId: input.attemptId,
786
- kind: "codex_turn_start",
787
- bodyBytes: Buffer.byteLength(input.text, "utf8"),
788
- bodySha256: createHash("sha256").update(input.text).digest("hex"),
789
- frameBytes: evidence.frameBytes,
790
- sha256: evidence.sha256,
791
- }),
792
- deadlineAt: input.deadlineAt,
793
- kind: "start",
794
- signal: controller.signal,
795
- route: {
796
- alias: input.targetAlias,
797
- hostId: input.binding.hostId,
798
- registrationId: input.binding.registrationId,
799
- threadId: input.binding.routeHandle,
800
- },
801
- text: content,
802
- onAccepted: async (current) => {
803
- if (this.closing ||
804
- this.closed ||
805
- current.attemptId !== input.attemptId ||
806
- this.activeTurns.has(input.binding.registrationId)) {
807
- throw new BridgeError("ACCEPTANCE_UNCONFIRMED", "The exact Codex attempt cannot acquire its active-turn slot.");
808
- }
809
- await input.onAccepted({ attemptId: input.attemptId });
810
- accepted = current;
811
- this.activeTurns.set(input.binding.registrationId, {
812
- attemptId: input.attemptId,
813
- accepted: current,
814
- alias: input.targetAlias,
815
- routeHandle: input.binding.routeHandle,
816
- });
817
- },
818
- });
819
- this.inFlightOperations.add(operation);
820
- try {
821
- const result = await operation;
822
- this.publishOperationObservation(input, result);
823
- return mapCodexStartResult(result, input.expectsReply);
824
- }
825
- finally {
826
- this.inFlightOperations.delete(operation);
827
- this.operationControllers.delete(controller);
828
- if (accepted !== undefined &&
829
- this.activeTurns.get(input.binding.registrationId)?.attemptId ===
830
- input.attemptId) {
831
- this.activeTurns.delete(input.binding.registrationId);
832
- }
833
- }
834
- }
835
- async close() {
836
- if (this.closed || this.closing)
837
- return;
838
- this.closing = true;
839
- if (this.observationTimer !== undefined) {
840
- this.timers.clearTimeout(this.observationTimer);
841
- this.observationTimer = undefined;
842
- }
843
- this.observationController?.abort();
844
- for (const controller of this.operationControllers)
845
- controller.abort();
846
- await this.observationInFlight?.catch(() => undefined);
847
- for (const operation of this.inFlightOperations) {
848
- void operation.catch(() => undefined);
849
- }
850
- this.operationControllers.clear();
851
- this.activeTurns.clear();
852
- this.observedRoutes.clear();
853
- this.callbacks = undefined;
854
- this.initialized = false;
855
- this.closed = true;
856
- this.closing = false;
857
- }
858
- publishOperationObservation(input, result) {
859
- const state = !result.cleanupConfirmed
860
- ? { state: "unobserved", safeErrorCode: "CLEANUP_FAILED" }
861
- : result.phase === "terminal"
862
- ? { state: "idle" }
863
- : result.safeErrorCode === "ROUTE_BUSY"
864
- ? { state: "busy" }
865
- : result.safeErrorCode === "APPROVAL_REQUIRED"
866
- ? { state: "awaiting_approval" }
867
- : result.safeErrorCode === "THREAD_NOT_OBSERVED"
868
- ? {
869
- state: "unobserved",
870
- safeErrorCode: "THREAD_NOT_OBSERVED",
871
- }
872
- : {
873
- state: "unobserved",
874
- safeErrorCode: "CODEX_OBSERVER_UNAVAILABLE",
875
- };
876
- this.emitObservation({
877
- alias: input.targetAlias,
878
- registrationId: input.binding.registrationId,
879
- routeHandle: input.binding.routeHandle,
880
- }, state);
881
- }
882
- scheduleObservation() {
883
- if (!this.initialized ||
884
- this.closing ||
885
- this.closed ||
886
- this.observationInFlight !== undefined) {
887
- return;
888
- }
889
- if (this.observationTimer !== undefined) {
890
- this.timers.clearTimeout(this.observationTimer);
891
- this.observationTimer = undefined;
892
- }
893
- const next = [...this.observedRoutes.values()].reduce((selected, candidate) => selected === undefined || candidate.nextAt < selected.nextAt
894
- ? candidate
895
- : selected, undefined);
896
- if (next === undefined)
897
- return;
898
- const delay = Math.max(0, next.nextAt - this.now().getTime());
899
- const timer = this.timers.setTimeout(() => {
900
- if (this.observationTimer !== timer)
901
- return;
902
- this.observationTimer = undefined;
903
- const operation = this.runObservation(next);
904
- this.observationInFlight = operation;
905
- void operation.finally(() => {
906
- if (this.observationInFlight === operation) {
907
- this.observationInFlight = undefined;
908
- }
909
- this.scheduleObservation();
910
- });
911
- }, delay);
912
- timer.unref();
913
- this.observationTimer = timer;
914
- }
915
- async runObservation(route) {
916
- const controller = new AbortController();
917
- this.observationController = controller;
918
- let observation;
919
- const timeout = this.timers.setTimeout(() => controller.abort(), this.observationTimeoutMs);
920
- timeout.unref();
921
- try {
922
- observation = await this.operation.observe({
923
- alias: route.alias,
924
- hostId: this.hostId,
925
- registrationId: route.registrationId,
926
- threadId: route.routeHandle,
927
- }, controller.signal);
928
- }
929
- finally {
930
- this.timers.clearTimeout(timeout);
931
- if (this.observationController === controller) {
932
- this.observationController = undefined;
933
- }
934
- }
935
- const current = this.observedRoutes.get(route.registrationId);
936
- if (current !== route || this.closing || this.closed)
937
- return;
938
- const failed = observation.state === "unobserved";
939
- current.attempt = failed ? Math.min(current.attempt + 1, 16) : 0;
940
- const delay = failed
941
- ? Math.min(this.observationBackoffMaxMs, this.observationPollMs * 2 ** Math.max(0, current.attempt - 1))
942
- : this.observationPollMs;
943
- current.nextAt = this.now().getTime() + delay;
944
- this.emitObservation(current, observation);
945
- }
946
- emitObservation(route, observation) {
947
- const current = this.observedRoutes.get(route.registrationId);
948
- if (this.callbacks === undefined ||
949
- current === undefined ||
950
- current.alias !== route.alias ||
951
- current.routeHandle !== route.routeHandle) {
952
- return;
953
- }
954
- invokeCallback(() => this.callbacks.onRouteState({
955
- route: {
956
- provider: "codex",
957
- hostId: this.hostId,
958
- routeHandle: route.routeHandle,
959
- registrationId: route.registrationId,
960
- },
961
- state: observation.state,
962
- observedAt: this.now().toISOString(),
963
- ...(observation.safeErrorCode === undefined
964
- ? {}
965
- : { safeErrorCode: observation.safeErrorCode }),
966
- }));
967
- }
968
- }
969
- export function createLocalCodexGatewayProvider(options) {
970
- return new LocalCodexGatewayProvider(options);
971
- }
972
- //# sourceMappingURL=providers.js.map