agent-embassy 1.8.2 → 1.9.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 (76) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +23 -3
  3. package/README.zh-CN.md +23 -3
  4. package/SECURITY.md +30 -6
  5. package/dist/src/gateway/cli-copy.en.d.ts +1 -1
  6. package/dist/src/gateway/cli-copy.en.js +8 -0
  7. package/dist/src/gateway/cli-copy.en.js.map +1 -1
  8. package/dist/src/gateway/cli-copy.zh-CN.d.ts +1 -1
  9. package/dist/src/gateway/cli-copy.zh-CN.js +8 -0
  10. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
  11. package/dist/src/gateway/cli.d.ts +8 -3
  12. package/dist/src/gateway/cli.js +191 -19
  13. package/dist/src/gateway/cli.js.map +1 -1
  14. package/dist/src/gateway/config.d.ts +2 -0
  15. package/dist/src/gateway/config.js +1 -1
  16. package/dist/src/gateway/config.js.map +1 -1
  17. package/dist/src/gateway/control.d.ts +79 -25
  18. package/dist/src/gateway/control.js +98 -18
  19. package/dist/src/gateway/control.js.map +1 -1
  20. package/dist/src/gateway/dashboard-copy.d.ts +1 -1
  21. package/dist/src/gateway/dashboard-copy.en.d.ts +2 -0
  22. package/dist/src/gateway/dashboard-copy.en.js +2 -0
  23. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  24. package/dist/src/gateway/dashboard-copy.js +2 -0
  25. package/dist/src/gateway/dashboard-copy.js.map +1 -1
  26. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +2 -0
  27. package/dist/src/gateway/dashboard-copy.zh-CN.js +2 -0
  28. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  29. package/dist/src/gateway/dashboard-model.d.ts +3 -0
  30. package/dist/src/gateway/dashboard-model.js +3 -0
  31. package/dist/src/gateway/dashboard-model.js.map +1 -1
  32. package/dist/src/gateway/federation-nodes.d.ts +18 -0
  33. package/dist/src/gateway/federation-nodes.js +142 -0
  34. package/dist/src/gateway/federation-nodes.js.map +1 -0
  35. package/dist/src/gateway/live-dashboard-app/app.js +5 -4
  36. package/dist/src/gateway/peer-client.d.ts +57 -0
  37. package/dist/src/gateway/peer-client.js +208 -0
  38. package/dist/src/gateway/peer-client.js.map +1 -0
  39. package/dist/src/gateway/peer-mailbox.d.ts +78 -0
  40. package/dist/src/gateway/peer-mailbox.js +168 -0
  41. package/dist/src/gateway/peer-mailbox.js.map +1 -0
  42. package/dist/src/gateway/peer-protocol.d.ts +107 -0
  43. package/dist/src/gateway/peer-protocol.js +72 -0
  44. package/dist/src/gateway/peer-protocol.js.map +1 -0
  45. package/dist/src/gateway/peer-stdio.d.ts +23 -0
  46. package/dist/src/gateway/peer-stdio.js +100 -0
  47. package/dist/src/gateway/peer-stdio.js.map +1 -0
  48. package/dist/src/gateway/provenance-envelope.js +6 -1
  49. package/dist/src/gateway/provenance-envelope.js.map +1 -1
  50. package/dist/src/gateway/providers.d.ts +5 -2
  51. package/dist/src/gateway/providers.js +10 -7
  52. package/dist/src/gateway/providers.js.map +1 -1
  53. package/dist/src/gateway/server.d.ts +4 -1
  54. package/dist/src/gateway/server.js +21 -8
  55. package/dist/src/gateway/server.js.map +1 -1
  56. package/dist/src/gateway/service.d.ts +19 -0
  57. package/dist/src/gateway/service.js +437 -37
  58. package/dist/src/gateway/service.js.map +1 -1
  59. package/dist/src/gateway/state-v2-to-v3.js +1 -0
  60. package/dist/src/gateway/state-v2-to-v3.js.map +1 -1
  61. package/dist/src/gateway/store.d.ts +9 -1
  62. package/dist/src/gateway/store.js +183 -43
  63. package/dist/src/gateway/store.js.map +1 -1
  64. package/dist/src/gateway/types.d.ts +7 -6
  65. package/dist/src/gateway/types.js +5 -10
  66. package/dist/src/gateway/types.js.map +1 -1
  67. package/docs/CONFIGURATION.md +23 -8
  68. package/docs/CONFIGURATION.zh-CN.md +12 -4
  69. package/docs/DASHBOARD.md +2 -2
  70. package/docs/DASHBOARD.zh-CN.md +2 -2
  71. package/docs/DELIVERY.md +3 -1
  72. package/docs/DELIVERY.zh-CN.md +3 -1
  73. package/docs/GATEWAY-ARCHITECTURE.md +6 -4
  74. package/package.json +1 -1
  75. package/skills/embassy-peer/SKILL.md +32 -4
  76. package/skills/embassy-peer/agents/openai.yaml +2 -2
@@ -1,11 +1,13 @@
1
1
  import type { GatewayPublicSnapshot } from "./types.js";
2
+ import { type PeerCatalogResult, type PeerHandoffParams, type PeerHandoffResult } from "./peer-protocol.js";
3
+ import { type PeerMailboxAwaitResult } from "./peer-mailbox.js";
2
4
  export declare const GATEWAY_CONTROL_PROTOCOL_VERSION: 1;
3
5
  export declare const GATEWAY_CONTROL_MAX_FRAME_BYTES: number;
4
6
  export declare const GATEWAY_CONTROL_MAX_RESPONSE_BYTES: number;
5
7
  export declare const GATEWAY_CONTROL_MAX_MESSAGE_BYTES: number;
6
8
  export declare const GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS = 3000;
7
9
  export declare const GATEWAY_CONTROL_MAX_CONNECTIONS = 32;
8
- export declare const gatewayControlMethods: readonly ["health", "register_codex", "unregister_codex", "remove_codex_registration", "select_claude", "unselect_claude", "pair", "unpair", "list_snapshot", "observe_snapshot", "delivery_status", "untrack", "send_to_claude", "send_to_codex", "reply", "refresh_dashboard"];
10
+ export declare const gatewayControlMethods: readonly ["health", "register_codex", "unregister_codex", "remove_codex_registration", "select_claude", "unselect_claude", "pair", "unpair", "list_snapshot", "observe_snapshot", "delivery_status", "untrack", "send_to_claude", "send_to_codex", "reply", "refresh_dashboard", "peer_catalog", "peer_handoff", "register_peer", "unregister_peer", "await_peer", "peer_receipt"];
9
11
  export type GatewayControlMethod = (typeof gatewayControlMethods)[number];
10
12
  export type GatewayBusyPolicy = "queue";
11
13
  export type RegisterCodexParams = {
@@ -51,38 +53,44 @@ export declare function pairParamThreadAttestation(params: Readonly<PairParams>)
51
53
  alias: string;
52
54
  threadId: string;
53
55
  }> | undefined;
54
- export type SendToClaudeParams = {
56
+ type SendBase = {
55
57
  fromAlias: string;
56
- threadId: string;
57
58
  toAlias: string;
58
59
  text: string;
59
60
  expectsReply?: boolean;
60
61
  trackIdleMinutes?: number;
61
62
  };
62
- export type ValidatedSendToClaudeParams = {
63
- fromAlias: string;
64
- threadId: string;
65
- toAlias: string;
66
- text: string;
63
+ type ValidatedSendBase = Omit<SendBase, "expectsReply"> & {
67
64
  expectsReply: boolean;
68
- trackIdleMinutes?: number;
69
- };
70
- export type SendToCodexParams = {
71
- fromAlias: string;
72
- toAlias: string;
73
- text: string;
74
- replyAddress?: string;
75
- expectsReply?: boolean;
76
- trackIdleMinutes?: number;
77
- };
78
- export type ValidatedSendToCodexParams = {
79
- fromAlias: string;
80
- toAlias: string;
81
- text: string;
82
- replyAddress?: string;
83
- expectsReply: boolean;
84
- trackIdleMinutes?: number;
85
65
  };
66
+ export type SendToClaudeParams = SendBase & ({
67
+ threadId: string;
68
+ peerToken?: never;
69
+ } | {
70
+ peerToken: string;
71
+ threadId?: never;
72
+ });
73
+ export type ValidatedSendToClaudeParams = ValidatedSendBase & ({
74
+ threadId: string;
75
+ peerToken?: never;
76
+ } | {
77
+ peerToken: string;
78
+ threadId?: never;
79
+ });
80
+ export type SendToCodexParams = SendBase & ({
81
+ replyAddress: string;
82
+ peerToken?: never;
83
+ } | {
84
+ peerToken: string;
85
+ replyAddress?: never;
86
+ });
87
+ export type ValidatedSendToCodexParams = ValidatedSendBase & ({
88
+ replyAddress: string;
89
+ peerToken?: never;
90
+ } | {
91
+ peerToken: string;
92
+ replyAddress?: never;
93
+ });
86
94
  export type GatewayReplyCaller = {
87
95
  kind: "codex";
88
96
  alias: string;
@@ -91,6 +99,10 @@ export type GatewayReplyCaller = {
91
99
  kind: "claude";
92
100
  alias: string;
93
101
  replyAddress?: string;
102
+ } | {
103
+ kind: "peer";
104
+ alias: string;
105
+ token: string;
94
106
  };
95
107
  export type ReplyParams = {
96
108
  conversationId: string;
@@ -104,6 +116,24 @@ export type UntrackParams = {
104
116
  export type DeliveryStatusParams = {
105
117
  token: string;
106
118
  };
119
+ export type PeerCatalogParams = {
120
+ peerHost: string;
121
+ };
122
+ export type PeerHandoffControlParams = {
123
+ peerHost: string;
124
+ handoff: PeerHandoffParams;
125
+ };
126
+ export type RegisterPeerParams = {
127
+ alias: string;
128
+ token?: string;
129
+ };
130
+ export type PeerPrincipalParams = {
131
+ alias: string;
132
+ token: string;
133
+ };
134
+ export type PeerReceiptParams = PeerPrincipalParams & {
135
+ receipt: string;
136
+ };
107
137
  type RequestParams = {
108
138
  health: Record<string, never>;
109
139
  register_codex: RegisterCodexParams;
@@ -121,6 +151,12 @@ type RequestParams = {
121
151
  send_to_codex: SendToCodexParams;
122
152
  reply: ReplyParams;
123
153
  refresh_dashboard: Record<string, never>;
154
+ peer_catalog: PeerCatalogParams;
155
+ peer_handoff: PeerHandoffControlParams;
156
+ register_peer: RegisterPeerParams;
157
+ unregister_peer: PeerPrincipalParams;
158
+ await_peer: PeerPrincipalParams;
159
+ peer_receipt: PeerReceiptParams;
124
160
  };
125
161
  export type GatewayControlRequest = {
126
162
  [M in GatewayControlMethod]: {
@@ -136,6 +172,7 @@ export type GatewayDecision = {
136
172
  } | {
137
173
  accepted: false;
138
174
  code: Exclude<GatewayDecisionCode, "ok">;
175
+ ownerHost?: string;
139
176
  };
140
177
  export type GatewaySendResult = {
141
178
  accepted: true;
@@ -170,6 +207,11 @@ export type GatewaySnapshotObservation = {
170
207
  snapshotRevision: number;
171
208
  snapshot: GatewaySnapshot;
172
209
  };
210
+ export type GatewayRegisterPeerResult = GatewayDecision | {
211
+ accepted: true;
212
+ code: "ok";
213
+ token: string;
214
+ };
173
215
  type ResultByMethod = {
174
216
  health: GatewayHealthResult;
175
217
  register_codex: GatewayDecision;
@@ -187,6 +229,12 @@ type ResultByMethod = {
187
229
  send_to_codex: GatewaySendResult;
188
230
  reply: GatewaySendResult;
189
231
  refresh_dashboard: GatewayRefreshResult;
232
+ peer_catalog: PeerCatalogResult;
233
+ peer_handoff: PeerHandoffResult;
234
+ register_peer: GatewayRegisterPeerResult;
235
+ unregister_peer: GatewayDecision;
236
+ await_peer: PeerMailboxAwaitResult;
237
+ peer_receipt: GatewayDecision;
190
238
  };
191
239
  type MaybePromise<T> = T | Promise<T>;
192
240
  export type GatewayControlHandlers = {
@@ -206,6 +254,12 @@ export type GatewayControlHandlers = {
206
254
  sendToCodex: (params: Readonly<ValidatedSendToCodexParams>) => MaybePromise<GatewaySendResult>;
207
255
  reply: (params: Readonly<ReplyParams>) => MaybePromise<GatewaySendResult>;
208
256
  refreshDashboard: () => MaybePromise<GatewayRefreshResult>;
257
+ peerCatalog?: (params: Readonly<PeerCatalogParams>) => MaybePromise<PeerCatalogResult>;
258
+ peerHandoff?: (params: Readonly<PeerHandoffControlParams>) => MaybePromise<PeerHandoffResult>;
259
+ registerPeer: (params: Readonly<RegisterPeerParams>) => MaybePromise<GatewayRegisterPeerResult>;
260
+ unregisterPeer: (params: Readonly<PeerPrincipalParams>) => MaybePromise<GatewayDecision>;
261
+ awaitPeer: (params: Readonly<PeerPrincipalParams>) => MaybePromise<PeerMailboxAwaitResult>;
262
+ peerReceipt: (params: Readonly<PeerReceiptParams>) => MaybePromise<GatewayDecision>;
209
263
  };
210
264
  export type GatewayWireErrorCode = "INVALID_JSON" | "FRAME_TOO_LARGE" | "INVALID_REQUEST" | "UNSUPPORTED_VERSION" | "UNKNOWN_METHOD" | "MULTIPLE_FRAMES" | "SERVER_BUSY" | "REQUEST_TIMEOUT" | "HANDLER_FAILURE" | "INVALID_HANDLER_RESPONSE" | "RESPONSE_TOO_LARGE";
211
265
  export type GatewayControlErrorResponse = {
@@ -5,6 +5,8 @@ import net from "node:net";
5
5
  import path from "node:path";
6
6
  import { TextDecoder } from "node:util";
7
7
  import { CONNECTOR_OBSERVATION_STALE_AFTER_MS, GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayProviders, gatewayPublicSnapshotLimits, isGatewayProvider, isMessageDirection, isPublicRegistryObservationSnapshot, } from "./types.js";
8
+ import { decodePeerParams, decodePeerResult } from "./peer-protocol.js";
9
+ import { isPeerMailboxAwaitResult } from "./peer-mailbox.js";
8
10
  export const GATEWAY_CONTROL_PROTOCOL_VERSION = 1;
9
11
  export const GATEWAY_CONTROL_MAX_FRAME_BYTES = 32 * 1024;
10
12
  export const GATEWAY_CONTROL_MAX_RESPONSE_BYTES = 256 * 1024;
@@ -19,6 +21,8 @@ const ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9
19
21
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
20
22
  const CONVERSATION_ID_PATTERN = /^conv_[A-Za-z0-9_-]{16,64}$/;
21
23
  const DELIVERY_TOKEN_PATTERN = /^dlv_[A-Za-z0-9_-]{24}$/;
24
+ const PEER_TOKEN_PATTERN = /^peer_[A-Za-z0-9_-]{32}$/;
25
+ const PEER_RECEIPT_PATTERN = /^prc_[A-Za-z0-9_-]{24}$/;
22
26
  const HOST_PATTERN = /^[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
23
27
  const SAFE_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
24
28
  const PROTOCOL_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
@@ -30,7 +34,8 @@ export const gatewayControlMethods = [
30
34
  "health", "register_codex", "unregister_codex", "remove_codex_registration",
31
35
  "select_claude", "unselect_claude", "pair", "unpair", "list_snapshot",
32
36
  "observe_snapshot", "delivery_status", "untrack", "send_to_claude",
33
- "send_to_codex", "reply", "refresh_dashboard",
37
+ "send_to_codex", "reply", "refresh_dashboard", "peer_catalog", "peer_handoff",
38
+ "register_peer", "unregister_peer", "await_peer", "peer_receipt",
34
39
  ];
35
40
  export function pairParamAliases(params) {
36
41
  return "aliases" in params ? params.aliases : [params.claudeAlias, params.codexAlias];
@@ -178,11 +183,62 @@ function arrayOf(value, maximum, check) {
178
183
  function positive(value) { return nonNegative(value) && value > 0; }
179
184
  function safeCode(value) { return typeof value === "string" && SAFE_CODE_PATTERN.test(value); }
180
185
  const messageText = isMessageText;
186
+ const peerToken = (value) => typeof value === "string" && PEER_TOKEN_PATTERN.test(value);
187
+ const peerAlias = (value) => alias(value) && value.startsWith("peer-");
181
188
  function emptyParams(value) {
182
189
  if (!isRecord(value) || !exact(value, []))
183
190
  invalid();
184
191
  return {};
185
192
  }
193
+ function decodePeerCatalog(value) {
194
+ if (!shape(value, { peerHost: host }))
195
+ invalid();
196
+ return { peerHost: value.peerHost };
197
+ }
198
+ function decodePeerHandoff(value) {
199
+ if (!isRecord(value) || !exact(value, ["peerHost", "handoff"]) || !host(value.peerHost))
200
+ invalid();
201
+ try {
202
+ return { peerHost: value.peerHost, handoff: decodePeerParams("handoff", value.handoff) };
203
+ }
204
+ catch {
205
+ return invalid();
206
+ }
207
+ }
208
+ function decodeRegisterPeer(value) {
209
+ if (!shape(value, { alias: peerAlias }, { token: peerToken }))
210
+ invalid();
211
+ return { alias: value.alias, ...(value.token === undefined ? {} : { token: value.token }) };
212
+ }
213
+ function decodePeerPrincipal(value) {
214
+ if (!shape(value, { alias: peerAlias, token: peerToken }))
215
+ invalid();
216
+ return { alias: value.alias, token: value.token };
217
+ }
218
+ function decodePeerReceipt(value) {
219
+ if (!shape(value, { alias: peerAlias, token: peerToken,
220
+ receipt: (item) => typeof item === "string" && PEER_RECEIPT_PATTERN.test(item) }))
221
+ invalid();
222
+ return { alias: value.alias, token: value.token, receipt: value.receipt };
223
+ }
224
+ const isPeerCatalog = (value) => {
225
+ try {
226
+ decodePeerResult("catalog/get", value);
227
+ return true;
228
+ }
229
+ catch {
230
+ return false;
231
+ }
232
+ };
233
+ const isPeerHandoffResult = (value) => {
234
+ try {
235
+ decodePeerResult("handoff", value);
236
+ return true;
237
+ }
238
+ catch {
239
+ return false;
240
+ }
241
+ };
186
242
  function decodeRegister(value) {
187
243
  if (!isRecord(value) || !exact(value, ["alias", "threadId"], ["hostId", "busyPolicy", "succeedsAlias"]) ||
188
244
  !alias(value.alias) || !value.alias.startsWith("codex-") || !uuid(value.threadId) ||
@@ -222,9 +278,7 @@ function decodePair(value) {
222
278
  if (Object.hasOwn(value, "aliases")) {
223
279
  if (!exact(value, ["aliases"], ["threadAttestation"]) || !Array.isArray(value.aliases) ||
224
280
  value.aliases.length !== 2 || !alias(value.aliases[0]) || !alias(value.aliases[1]) ||
225
- value.aliases[0] === value.aliases[1] ||
226
- value.aliases[0].slice(value.aliases[0].lastIndexOf("@") + 1) !==
227
- value.aliases[1].slice(value.aliases[1].lastIndexOf("@") + 1))
281
+ value.aliases[0] === value.aliases[1])
228
282
  invalid();
229
283
  let threadAttestation;
230
284
  if (value.threadAttestation !== undefined) {
@@ -258,24 +312,29 @@ function commonSend(value, required, optional) {
258
312
  return value;
259
313
  }
260
314
  function decodeSendClaude(value) {
261
- if (!isRecord(value) || !exact(value, ["fromAlias", "threadId", "toAlias", "text"], ["expectsReply", "trackIdleMinutes"]) ||
262
- !alias(value.fromAlias) || !uuid(value.threadId) || typeof value.toAlias !== "string" ||
315
+ if (!isRecord(value) || !exact(value, ["fromAlias", "toAlias", "text"], ["threadId", "peerToken", "expectsReply", "trackIdleMinutes"]) ||
316
+ !alias(value.fromAlias) || (value.threadId === undefined) === (value.peerToken === undefined) ||
317
+ (value.threadId !== undefined && !uuid(value.threadId)) ||
318
+ (value.peerToken !== undefined && !peerToken(value.peerToken)) || typeof value.toAlias !== "string" ||
263
319
  !isClaudeSessionSelector(value.toAlias) || !messageText(value.text) ||
264
320
  (value.expectsReply !== undefined && typeof value.expectsReply !== "boolean") ||
265
321
  (value.trackIdleMinutes !== undefined && !trackMinutes(value.trackIdleMinutes)))
266
322
  invalid();
267
- return { fromAlias: value.fromAlias, threadId: value.threadId.toLowerCase(),
323
+ return { fromAlias: value.fromAlias,
268
324
  toAlias: UUID_PATTERN.test(value.toAlias) ? value.toAlias.toLowerCase() : value.toAlias,
269
325
  text: value.text, expectsReply: value.expectsReply ?? false,
326
+ ...(typeof value.threadId === "string" ? { threadId: value.threadId.toLowerCase() } : { peerToken: value.peerToken }),
270
327
  ...(value.trackIdleMinutes === undefined ? {} : { trackIdleMinutes: value.trackIdleMinutes }) };
271
328
  }
272
329
  function decodeSendCodex(value) {
273
- const row = commonSend(value, ["fromAlias", "toAlias", "text"], ["replyAddress", "expectsReply", "trackIdleMinutes"]);
274
- if (row.replyAddress !== undefined &&
275
- (typeof row.replyAddress !== "string" || !isGatewayReplyAddress(row.replyAddress)))
330
+ const row = commonSend(value, ["fromAlias", "toAlias", "text"], ["replyAddress", "peerToken", "expectsReply", "trackIdleMinutes"]);
331
+ if ((row.replyAddress === undefined) === (row.peerToken === undefined) ||
332
+ (row.replyAddress !== undefined && (typeof row.replyAddress !== "string" || !isGatewayReplyAddress(row.replyAddress))) ||
333
+ (row.peerToken !== undefined && !peerToken(row.peerToken)))
276
334
  invalid();
335
+ const replyAddress = row.replyAddress !== undefined;
277
336
  return { fromAlias: row.fromAlias, toAlias: row.toAlias, text: row.text,
278
- ...(row.replyAddress === undefined ? {} : { replyAddress: row.replyAddress }),
337
+ ...(replyAddress ? { replyAddress: row.replyAddress } : { peerToken: row.peerToken }),
279
338
  expectsReply: row.expectsReply === true,
280
339
  ...(row.trackIdleMinutes === undefined ? {} : { trackIdleMinutes: row.trackIdleMinutes }) };
281
340
  }
@@ -290,6 +349,8 @@ function normalizeReplyCaller(value) {
290
349
  (typeof value.replyAddress === "string" && isGatewayReplyAddress(value.replyAddress))))
291
350
  return { kind: "claude", alias: value.alias,
292
351
  ...(value.replyAddress === undefined ? {} : { replyAddress: value.replyAddress }) };
352
+ if (value.kind === "peer" && shape(value, { kind: oneOf("peer"), alias: peerAlias, token: peerToken }))
353
+ return { kind: "peer", alias: value.alias, token: value.token };
293
354
  return invalid();
294
355
  }
295
356
  function decodeReply(value) {
@@ -312,7 +373,12 @@ function decodeUntrack(value) {
312
373
  return { conversationId: value.conversationId };
313
374
  }
314
375
  function isDecision(value) {
315
- return shape(value, { accepted: (item) => typeof item === "boolean", code: oneOf("ok", "not_found", "conflict", "watch_owner_conflict", "route_mismatch", "busy", "unavailable", "rejected") }) && (value.accepted === true ? value.code === "ok" : value.code !== "ok");
376
+ return shape(value, { accepted: (item) => typeof item === "boolean", code: oneOf("ok", "not_found", "conflict", "watch_owner_conflict", "route_mismatch", "busy", "unavailable", "rejected") }, { ownerHost: host }) && (value.accepted === true
377
+ ? value.code === "ok" && value.ownerHost === undefined
378
+ : value.code !== "ok" && (value.ownerHost === undefined || value.code === "conflict"));
379
+ }
380
+ function isRegisterPeerResult(value) {
381
+ return isDecision(value) || shape(value, { accepted: oneOf(true), code: oneOf("ok"), token: peerToken });
316
382
  }
317
383
  function isHealthResult(value) {
318
384
  return shape(value, { status: oneOf("ok", "degraded"), revision: nonNegative });
@@ -356,7 +422,7 @@ function isConnectorSnapshot(value) {
356
422
  function isRouteSnapshot(value) {
357
423
  if (!shape(value, { alias, provider: isGatewayProvider, host, enabled: (item) => typeof item === "boolean",
358
424
  state: oneOf("stale", "idle", "busy", "awaiting_approval", "offline", "disabled"),
359
- busyPolicy: oneOf("queue"), queueDepth: nonNegative, counters: isRouteCounters }, { lastSeenAt: iso, oldestQueuedAt: iso, safeErrorCode: safeCode }))
425
+ busyPolicy: oneOf("queue"), queueDepth: nonNegative, counters: isRouteCounters }, { lastSeenAt: iso, oldestQueuedAt: iso, safeErrorCode: safeCode, mutable: (item) => typeof item === "boolean" }))
360
426
  return false;
361
427
  const row = value;
362
428
  return row.alias.endsWith(`@${row.host}`) &&
@@ -369,11 +435,12 @@ function compareConsentEndpoints(left, right) {
369
435
  function isConsentEdgeSnapshot(value) {
370
436
  const endpoint = (item) => shape(item, { alias, provider: isGatewayProvider });
371
437
  if (!shape(value, { endpoints: (rows) => Array.isArray(rows) && rows.length === 2 && rows.every(endpoint),
372
- host, counters: isRouteCounters }))
438
+ host, counters: isRouteCounters }, { mutable: (item) => typeof item === "boolean" }))
373
439
  return false;
374
440
  const [left, right] = value.endpoints;
375
- return left.provider !== right.provider && left.alias !== right.alias && compareConsentEndpoints(left, right) < 0 &&
376
- left.alias.endsWith(`@${value.host}`) && right.alias.endsWith(`@${value.host}`);
441
+ const hosts = [left.alias.slice(left.alias.lastIndexOf("@") + 1), right.alias.slice(right.alias.lastIndexOf("@") + 1)];
442
+ return left.alias !== right.alias && compareConsentEndpoints(left, right) < 0 &&
443
+ (left.provider !== right.provider || hosts[0] !== hosts[1]) && value.host === [...hosts].sort()[0];
377
444
  }
378
445
  function isAvailablePeerSnapshot(value) {
379
446
  return shape(value, { alias, provider: oneOf("claude"), host,
@@ -500,6 +567,12 @@ const descriptors = {
500
567
  delivery_status: { handler: "deliveryStatus", decode: decodeDeliveryStatus, result: isDeliveryStatusResult, mutation: false }, untrack: { handler: "untrack", decode: decodeUntrack, result: isDecision, mutation: false },
501
568
  send_to_claude: { handler: "sendToClaude", decode: decodeSendClaude, result: isSendResult, mutation: true }, send_to_codex: { handler: "sendToCodex", decode: decodeSendCodex, result: isSendResult, mutation: true },
502
569
  reply: { handler: "reply", decode: decodeReply, result: isSendResult, mutation: true }, refresh_dashboard: { handler: "refreshDashboard", decode: emptyParams, result: isRefreshResult, mutation: false },
570
+ peer_catalog: { handler: "peerCatalog", decode: decodePeerCatalog, result: isPeerCatalog, mutation: false },
571
+ peer_handoff: { handler: "peerHandoff", decode: decodePeerHandoff, result: isPeerHandoffResult, mutation: true },
572
+ register_peer: { handler: "registerPeer", decode: decodeRegisterPeer, result: isRegisterPeerResult, mutation: true },
573
+ unregister_peer: { handler: "unregisterPeer", decode: decodePeerPrincipal, result: isDecision, mutation: true },
574
+ await_peer: { handler: "awaitPeer", decode: decodePeerPrincipal, result: isPeerMailboxAwaitResult, mutation: false },
575
+ peer_receipt: { handler: "peerReceipt", decode: decodePeerReceipt, result: isDecision, mutation: true },
503
576
  };
504
577
  function parseRequestObject(value) {
505
578
  if (!isRecord(value) || !exact(value, ["protocolVersion", "method", "params"]))
@@ -887,7 +960,7 @@ export async function startGatewayControlServer(options) {
887
960
  close,
888
961
  };
889
962
  }
890
- function decodeResponse(method, value) {
963
+ function decodeResponse(method, params, value) {
891
964
  if (!isRecord(value) || value.protocolVersion !== 1)
892
965
  throw controlTransportError("CONTROL_INVALID_RESPONSE");
893
966
  if (value.ok === false) {
@@ -899,6 +972,13 @@ function decodeResponse(method, value) {
899
972
  }
900
973
  if (!shape(value, { protocolVersion: oneOf(1), ok: oneOf(true), result: descriptors[method].result }))
901
974
  throw controlTransportError("CONTROL_INVALID_RESPONSE");
975
+ if (method === "register_peer" && isRecord(value.result) && value.result.accepted === true &&
976
+ Object.hasOwn(params, "token") === Object.hasOwn(value.result, "token"))
977
+ throw controlTransportError("CONTROL_INVALID_RESPONSE");
978
+ if (method === "await_peer" && isRecord(value.result) && value.result.state === "message" &&
979
+ JSON.parse(value.result.frame).result.toAlias !==
980
+ params.alias)
981
+ throw controlTransportError("CONTROL_INVALID_RESPONSE");
902
982
  return value;
903
983
  }
904
984
  export async function sendGatewayControlRequest(options) {
@@ -928,7 +1008,7 @@ export async function sendGatewayControlRequest(options) {
928
1008
  socket.once("connect", () => { writeStarted = true; socket.write(frame); });
929
1009
  void readOneFrame(socket, maximum).then((responseFrame) => {
930
1010
  try {
931
- const response = decodeResponse(options.request.method, decodeJson(responseFrame, "INVALID_JSON"));
1011
+ const response = decodeResponse(options.request.method, request.params, decodeJson(responseFrame, "INVALID_JSON"));
932
1012
  if (!settled) {
933
1013
  settled = true;
934
1014
  socket.destroy();