agent-embassy 1.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 (145) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/CONTRIBUTING.md +122 -0
  3. package/LICENSE +21 -0
  4. package/README.md +185 -0
  5. package/README.zh-CN.md +185 -0
  6. package/SECURITY.md +224 -0
  7. package/assets/live-dashboard/app.css +1610 -0
  8. package/assets/mark-seal.svg +7 -0
  9. package/assets/mark.svg +7 -0
  10. package/assets/social-preview.png +0 -0
  11. package/assets/vendor/react/LICENSE +21 -0
  12. package/assets/vendor/react/react-dom.production.min.js +267 -0
  13. package/assets/vendor/react/react.production.min.js +31 -0
  14. package/dist/src/errors.d.ts +5 -0
  15. package/dist/src/errors.js +11 -0
  16. package/dist/src/errors.js.map +1 -0
  17. package/dist/src/gateway/claude-compatibility-scratch.d.ts +18 -0
  18. package/dist/src/gateway/claude-compatibility-scratch.js +158 -0
  19. package/dist/src/gateway/claude-compatibility-scratch.js.map +1 -0
  20. package/dist/src/gateway/claude-helper-client.d.ts +43 -0
  21. package/dist/src/gateway/claude-helper-client.js +212 -0
  22. package/dist/src/gateway/claude-helper-client.js.map +1 -0
  23. package/dist/src/gateway/claude-helper-protocol.d.ts +172 -0
  24. package/dist/src/gateway/claude-helper-protocol.js +373 -0
  25. package/dist/src/gateway/claude-helper-protocol.js.map +1 -0
  26. package/dist/src/gateway/claude-helper-supervisor.d.ts +74 -0
  27. package/dist/src/gateway/claude-helper-supervisor.js +497 -0
  28. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -0
  29. package/dist/src/gateway/claude-helper.d.ts +1 -0
  30. package/dist/src/gateway/claude-helper.js +269 -0
  31. package/dist/src/gateway/claude-helper.js.map +1 -0
  32. package/dist/src/gateway/claude-peer.d.ts +273 -0
  33. package/dist/src/gateway/claude-peer.js +2359 -0
  34. package/dist/src/gateway/claude-peer.js.map +1 -0
  35. package/dist/src/gateway/claude-runtime.d.ts +40 -0
  36. package/dist/src/gateway/claude-runtime.js +302 -0
  37. package/dist/src/gateway/claude-runtime.js.map +1 -0
  38. package/dist/src/gateway/cli-copy.d.ts +8 -0
  39. package/dist/src/gateway/cli-copy.en.d.ts +13 -0
  40. package/dist/src/gateway/cli-copy.en.js +43 -0
  41. package/dist/src/gateway/cli-copy.en.js.map +1 -0
  42. package/dist/src/gateway/cli-copy.js +19 -0
  43. package/dist/src/gateway/cli-copy.js.map +1 -0
  44. package/dist/src/gateway/cli-copy.zh-CN.d.ts +13 -0
  45. package/dist/src/gateway/cli-copy.zh-CN.js +43 -0
  46. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -0
  47. package/dist/src/gateway/cli.d.ts +44 -0
  48. package/dist/src/gateway/cli.js +969 -0
  49. package/dist/src/gateway/cli.js.map +1 -0
  50. package/dist/src/gateway/codex-app-server.d.ts +310 -0
  51. package/dist/src/gateway/codex-app-server.js +1746 -0
  52. package/dist/src/gateway/codex-app-server.js.map +1 -0
  53. package/dist/src/gateway/codex-local-transport.d.ts +81 -0
  54. package/dist/src/gateway/codex-local-transport.js +617 -0
  55. package/dist/src/gateway/codex-local-transport.js.map +1 -0
  56. package/dist/src/gateway/codex-registration-generation.d.ts +5 -0
  57. package/dist/src/gateway/codex-registration-generation.js +19 -0
  58. package/dist/src/gateway/codex-registration-generation.js.map +1 -0
  59. package/dist/src/gateway/codex-registration-succession.d.ts +209 -0
  60. package/dist/src/gateway/codex-registration-succession.js +588 -0
  61. package/dist/src/gateway/codex-registration-succession.js.map +1 -0
  62. package/dist/src/gateway/compatibility.d.ts +70 -0
  63. package/dist/src/gateway/compatibility.js +252 -0
  64. package/dist/src/gateway/compatibility.js.map +1 -0
  65. package/dist/src/gateway/config.d.ts +24 -0
  66. package/dist/src/gateway/config.js +130 -0
  67. package/dist/src/gateway/config.js.map +1 -0
  68. package/dist/src/gateway/control.d.ts +308 -0
  69. package/dist/src/gateway/control.js +1573 -0
  70. package/dist/src/gateway/control.js.map +1 -0
  71. package/dist/src/gateway/dashboard-copy.d.ts +7 -0
  72. package/dist/src/gateway/dashboard-copy.en.d.ts +506 -0
  73. package/dist/src/gateway/dashboard-copy.en.js +507 -0
  74. package/dist/src/gateway/dashboard-copy.en.js.map +1 -0
  75. package/dist/src/gateway/dashboard-copy.js +516 -0
  76. package/dist/src/gateway/dashboard-copy.js.map +1 -0
  77. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +506 -0
  78. package/dist/src/gateway/dashboard-copy.zh-CN.js +507 -0
  79. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -0
  80. package/dist/src/gateway/dashboard-model.d.ts +207 -0
  81. package/dist/src/gateway/dashboard-model.js +720 -0
  82. package/dist/src/gateway/dashboard-model.js.map +1 -0
  83. package/dist/src/gateway/dashboard.d.ts +26 -0
  84. package/dist/src/gateway/dashboard.js +941 -0
  85. package/dist/src/gateway/dashboard.js.map +1 -0
  86. package/dist/src/gateway/delivery-machine.d.ts +235 -0
  87. package/dist/src/gateway/delivery-machine.js +540 -0
  88. package/dist/src/gateway/delivery-machine.js.map +1 -0
  89. package/dist/src/gateway/instance-lease.d.ts +21 -0
  90. package/dist/src/gateway/instance-lease.js +549 -0
  91. package/dist/src/gateway/instance-lease.js.map +1 -0
  92. package/dist/src/gateway/live-dashboard-app/app.js +2914 -0
  93. package/dist/src/gateway/live-dashboard-assets.d.ts +10 -0
  94. package/dist/src/gateway/live-dashboard-assets.js +73 -0
  95. package/dist/src/gateway/live-dashboard-assets.js.map +1 -0
  96. package/dist/src/gateway/live-dashboard-bootstrap.d.ts +36 -0
  97. package/dist/src/gateway/live-dashboard-bootstrap.js +176 -0
  98. package/dist/src/gateway/live-dashboard-bootstrap.js.map +1 -0
  99. package/dist/src/gateway/live-dashboard-command.d.ts +54 -0
  100. package/dist/src/gateway/live-dashboard-command.js +324 -0
  101. package/dist/src/gateway/live-dashboard-command.js.map +1 -0
  102. package/dist/src/gateway/live-dashboard-http.d.ts +41 -0
  103. package/dist/src/gateway/live-dashboard-http.js +445 -0
  104. package/dist/src/gateway/live-dashboard-http.js.map +1 -0
  105. package/dist/src/gateway/live-dashboard-protocol.d.ts +38 -0
  106. package/dist/src/gateway/live-dashboard-protocol.js +156 -0
  107. package/dist/src/gateway/live-dashboard-protocol.js.map +1 -0
  108. package/dist/src/gateway/live-dashboard-server.d.ts +33 -0
  109. package/dist/src/gateway/live-dashboard-server.js +130 -0
  110. package/dist/src/gateway/live-dashboard-server.js.map +1 -0
  111. package/dist/src/gateway/live-dashboard-stream.d.ts +46 -0
  112. package/dist/src/gateway/live-dashboard-stream.js +233 -0
  113. package/dist/src/gateway/live-dashboard-stream.js.map +1 -0
  114. package/dist/src/gateway/live-dashboard.d.ts +31 -0
  115. package/dist/src/gateway/live-dashboard.js +190 -0
  116. package/dist/src/gateway/live-dashboard.js.map +1 -0
  117. package/dist/src/gateway/locale.d.ts +4 -0
  118. package/dist/src/gateway/locale.js +10 -0
  119. package/dist/src/gateway/locale.js.map +1 -0
  120. package/dist/src/gateway/progress-watch-machine.d.ts +97 -0
  121. package/dist/src/gateway/progress-watch-machine.js +172 -0
  122. package/dist/src/gateway/progress-watch-machine.js.map +1 -0
  123. package/dist/src/gateway/providers.d.ts +287 -0
  124. package/dist/src/gateway/providers.js +2451 -0
  125. package/dist/src/gateway/providers.js.map +1 -0
  126. package/dist/src/gateway/server.d.ts +57 -0
  127. package/dist/src/gateway/server.js +264 -0
  128. package/dist/src/gateway/server.js.map +1 -0
  129. package/dist/src/gateway/service.d.ts +449 -0
  130. package/dist/src/gateway/service.js +4831 -0
  131. package/dist/src/gateway/service.js.map +1 -0
  132. package/dist/src/gateway/store.d.ts +340 -0
  133. package/dist/src/gateway/store.js +4047 -0
  134. package/dist/src/gateway/store.js.map +1 -0
  135. package/dist/src/gateway/types.d.ts +514 -0
  136. package/dist/src/gateway/types.js +414 -0
  137. package/dist/src/gateway/types.js.map +1 -0
  138. package/dist/src/mutex.d.ts +4 -0
  139. package/dist/src/mutex.js +23 -0
  140. package/dist/src/mutex.js.map +1 -0
  141. package/docs/DESIGN.md +134 -0
  142. package/docs/GATEWAY-ARCHITECTURE.md +858 -0
  143. package/package.json +80 -0
  144. package/skills/embassy-peer/SKILL.md +198 -0
  145. package/skills/embassy-peer/agents/openai.yaml +4 -0
@@ -0,0 +1,858 @@
1
+ # Embassy Gateway Architecture
2
+
3
+ Status: local bidirectional version 1 implemented and live-tested with one
4
+ advertised Codex task; remote connectors remain deferred. The published v1
5
+ package supports macOS, the only platform exercised end to end so far.
6
+
7
+ This document uses four evidence labels:
8
+
9
+ - **Official**: documented by Anthropic or OpenAI.
10
+ - **Implemented**: present in this worktree and covered by deterministic tests.
11
+ - **Observed**: established by a bounded, read-only local feasibility probe.
12
+ - **Planned**: designed but not yet integrated or live-validated.
13
+
14
+ A bounded real test completed native Claude discovery and messaging, held a
15
+ message while the registered Codex task was active, automatically started the
16
+ queued turn after idle, and delivered the exact final reply back to Claude.
17
+
18
+ ## Purpose and boundary
19
+
20
+ The gateway lets already-running Claude Code sessions and explicitly
21
+ registered native Codex tasks address one another by short aliases. Outbound
22
+ Codex-to-Claude sends require an explicitly selected Claude route; inbound
23
+ native Claude messages may come from any exact compatible live same-UID
24
+ session without making that session outbound-selected. It provides a single
25
+ private operational view across the two products without rebuilding either
26
+ agent runtime.
27
+
28
+ Its exact Claude Code 2.1.226 runtime/peer-protocol pin is fail-closed.
29
+ Still-running 2.1.224 sessions remain compatible during a patch upgrade
30
+ because their registry records use the same reviewed peer protocol 1 shape.
31
+
32
+ It is deliberately:
33
+
34
+ - personal, local, same-OS-user software;
35
+ - single-user and non-hosted;
36
+ - an alias router and bounded message broker, not an agent runtime;
37
+ - unable to create Codex sidebar task cards or Claude session UI.
38
+
39
+ Embassy uses one private same-user Unix-domain control socket for its thin
40
+ clients and generates a private static dashboard page in each supported
41
+ language. `embassy serve` does not add a TCP listener, HTTP server, or public
42
+ API. The opt-in `embassy dashboard --live` companion is a separate foreground
43
+ process that binds an authenticated listener with three bounded route-consent
44
+ actions on `127.0.0.1`; it is
45
+ described under [Live dashboard companion](#live-dashboard-companion).
46
+
47
+ ### Why this uses the new feature, but is not skill-only
48
+
49
+ The gateway does not use Claude Channels. Claude's official cross-session
50
+ feature supplies genuine Claude-session discovery, inbound policy, and native
51
+ message delivery. It does not define a third-party session kind or make Codex
52
+ tasks appear in `ListAgents`, so it cannot by itself provide the symmetric
53
+ Claude–Codex address book the user wants.
54
+
55
+ The repo-shipped skill is the lightweight user/agent interface. A persistent
56
+ local broker is still required to own the private control socket, retain
57
+ transient reply correlation, watch endpoint generations, queue while a Codex
58
+ task is busy, and regenerate the dashboard between agent turns. The skill does
59
+ not wrap, replace, or recreate either provider.
60
+
61
+ ## What is official and what is internal
62
+
63
+ ### Claude Code
64
+
65
+ **Official:** Claude Code 2.1.226 documents cross-session messaging on macOS
66
+ and Linux. Real Claude sessions can use `ListAgents` to find other real Claude
67
+ sessions and `SendMessage` to contact them. A target can accept, hold, or
68
+ refuse inbound cross-session messages through `crossSessionInbound`. Messages
69
+ do not bypass the receiver's tool permissions or approval boundary.
70
+
71
+ **Version-pinned internal boundary:** the installed Claude Code 2.1.226 build
72
+ advertises live sessions through registry records and transports peer frames
73
+ over per-session Unix-domain sockets using peer protocol 1. Those registry and
74
+ wire shapes are not documented as a stable third-party integration API. The
75
+ gateway therefore pins the exact Claude Code version and protocol, validates
76
+ every record and socket immediately before use, and fails closed after an
77
+ update until the adapter is reviewed again.
78
+
79
+ For the lowest-impedance native path, the gateway publishes one process-owned
80
+ registry record whose name is visibly prefixed `codex-`. The listener remains
81
+ gateway-owned and does not claim to be a Claude model session; the explicit
82
+ name is the product boundary. The record uses the version-pinned native peer
83
+ shape so Claude's own `ListAgents` and `SendMessage` tools work unchanged.
84
+
85
+ Consequences:
86
+
87
+ - Native Claude `ListAgents` discovers real Claude sessions plus the one
88
+ explicitly named `codex-*` gateway peer.
89
+ - The gateway discovers compatible real Claude sessions as transient
90
+ candidates, but publishes only sanitized aliases and state. A send from a
91
+ registered Codex task may address only an explicitly selected route by its
92
+ current name or UUID. Per-message consent stays native: delivery lands in the
93
+ Claude session's own `crossSessionInbound` policy and approval flow.
94
+ - Codex aliases are discovered through the gateway CLI/skill and dashboard,
95
+ not through `ListAgents`.
96
+ - A gateway-owned anonymous callback UDS can receive a correlated reply. It
97
+ does not need, and must not create, a Claude registry record.
98
+
99
+ ### Codex
100
+
101
+ **Official:** Codex App Server is the JSON-RPC interface used by rich Codex
102
+ clients. Its Unix-socket transport is WebSocket over a standard HTTP Upgrade.
103
+ The documented protocol includes loaded-thread discovery, thread resume,
104
+ turn start, turn steer, turn interrupt, and notifications. Embassy exposes
105
+ `turn/steer` only behind the exact Claude-to-Codex `STEER:` contract described
106
+ below; there is no generic RPC surface.
107
+
108
+ **Official:** for an SSH project, the ChatGPT desktop app starts the remote
109
+ Codex App Server through SSH using the remote user's login shell. Files,
110
+ commands, credentials, permissions, plugins, skills, and local tools come from
111
+ that execution host. App Server transports should not be exposed directly on
112
+ a shared or public network.
113
+
114
+ **Observed:** this Desktop build connects to a host-local App Server on each
115
+ execution host. Remote tasks on `build-mac` do not route through the local App
116
+ Server. Desktop reaches the remote listener through an SSH `app-server proxy`.
117
+ A second attach-only client successfully initialized against the already-owned
118
+ `build-mac` listener and called only `thread/loaded/list` without creating a turn.
119
+ The same topology is expected for `lab-mac.example`, but that host has not been
120
+ probed by this project.
121
+
122
+ ## Topology
123
+
124
+ ```text
125
+ real Claude sessions (this Mac)
126
+ │ genuine session sockets
127
+
128
+ ├──────────────┐
129
+ │ │ callback replies
130
+ ▼ ▼
131
+ ┌──────────────────────── local singleton gateway ───────────────────────┐
132
+ │ private control UDS │ transient bodies │ metadata state │ static HTML │
133
+ └──────────┬──────────┴──────────────────┴────────────────┴──────────────┘
134
+
135
+ ├─ local Codex App Server ─ registered native local tasks
136
+
137
+ ├─ planned attach-only SSH proxy ─ build-mac App Server
138
+
139
+ └─ planned attach-only SSH proxy ─ lab-mac.example App Server
140
+
141
+ Claude-side skill/CLI ─ private control UDS ─ gateway
142
+ Codex-side skill/CLI ─ private control UDS ─ gateway
143
+ ```
144
+
145
+ The local singleton is necessary even though user interaction can be packaged
146
+ as a skill. A skill runs during an agent turn; it cannot remain discoverable,
147
+ hold transient correlation state, accept an inbound socket connection, or
148
+ wake a different idle runtime after that turn ends.
149
+
150
+ ## Component status
151
+
152
+ The status below is intentionally narrower than the target architecture.
153
+
154
+ | Component | Current evidence |
155
+ | --- | --- |
156
+ | Neutral gateway types, metadata store, route fencing, bounded queues, dedupe, rate limits, and public projection | **Implemented**, deterministic tests; message bodies remain memory-only |
157
+ | Private JSONL control protocol over a controller-owned UDS | **Implemented**, deterministic synthetic tests; no provider connection required |
158
+ | Static metadata-only dashboard renderer and atomic publisher | **Implemented**, deterministic security tests; the static renderer requires no browser or HTTP server |
159
+ | Opt-in live dashboard companion (`embassy dashboard --live`) | **Implemented**, deterministic tests over the loopback listener, capability-to-cookie exchange, projection, and three bounded route-consent actions; it is a separate foreground process, never part of `embassy serve` |
160
+ | Claude registry/peer adapter pinned to 2.1.226 / peer protocol 1 | **Implemented** and live-tested, including 2.1.224–2.1.226 patch-overlap discovery, print-session discovery, native status frames, cancellation, and accessible-workspace attestation |
161
+ | Exact Claude 2.1.226 binary/runtime attestation | **Implemented**; executes only bounded `claude --version` with a scrubbed environment and derives but does not open provider roots |
162
+ | Allowlisted Codex App Server connector with bounded busy behavior | **Implemented** and live-tested against App Server 0.147.0 for external busy observation, registered-route reachability across settings changes, and an automatically started queued turn; exact `STEER:` boundary behavior is covered deterministically |
163
+ | Attach-only local Codex proxy transport and exact-owned cleanup | **Implemented**, five deterministic tests; no live App Server connection in routine tests |
164
+ | Local provider adapters | **Implemented**, focused synthetic tests cover genuine-interactive Claude discovery, exact send/callback/receipt settlement and post-dispatch refresh, plus exact opted-in Codex ownership, registered-route reachability, monitor-only fallback, and cleanup; remote adapters remain disabled |
165
+ | Gateway service composition | **Implemented**, including private control-server startup, adapter lifecycle, synthetic cross-provider selection/dispatch/reply correlation, metadata-only publication, and clean-restart abandonment tests |
166
+ | Delivery receipt/status lifecycle | **Implemented**, deterministic synthetic tests cover stable-UUID native receipt re-resolution, the merged/verbose/quiet Claude notice policy, one bounded stall notice with pending age where enabled, opaque memory-only correlation handles, the closed status/terminal schema, and one-shot/bounded-wait CLI behavior |
167
+ | Operator/agent client CLI and package binary | **Implemented**, deterministic private-UDS tests cover the closed command family, inherited provider identity, bounded stdin-only bodies, normalized output, and ambiguous no-retry behavior |
168
+ | Repo-shipped cross-provider skill | **Implemented** as a repo-scoped workflow over the client CLI; it is not installed into either provider's global configuration |
169
+ | Foreground local broker launcher and provider assembly | **Implemented** as `embassy serve`; local-host-only with native messaging enabled |
170
+ | Live Codex-to-Claude delivery | **Tested** through selected real Claude 2.1.224–2.1.226 sessions |
171
+ | Claude-initiated Codex turn/reply into Codex | **Tested** with a real busy Codex task: native `busy → waiting`, automatic post-idle turn, terminal delivery status, and exact reply round trip |
172
+ | Remote production connector | **Planned**; only the `build-mac` read-only attach feasibility probe is complete |
173
+
174
+ Synthetic tests do not scan `~/.claude`, connect `/tmp/cc-socks`, attach to a
175
+ Desktop App Server, invoke SSH, or make a model request.
176
+
177
+ ## Identity, discovery, and opt-in
178
+
179
+ Users address Codex routes by strict aliases and Claude routes by either the
180
+ session's latest alias or its native session UUID, for example:
181
+
182
+ ```text
183
+ codex-reviewer@this-mac
184
+ codex-builder@build-mac
185
+ codex-release-check@lab-mac.example
186
+ claude-advisor@this-mac
187
+ 123e4567-e89b-42d3-a456-426614174000
188
+ ```
189
+
190
+ Claude's native `sessionId` UUID is its sole logical identity. Its current
191
+ name is a mutable lookup alias for that UUID; the gateway keeps no historical
192
+ name index. A rename therefore makes the old name stop resolving immediately,
193
+ while the UUID and an already selected UUID-bound route continue to identify
194
+ the same session. PID, registry path, process generation, and socket generation
195
+ are replaceable delivery coordinates, not identity, and are refreshed from the
196
+ live registry before a write. The gateway rejects duplicate current names and
197
+ duplicate live UUIDs rather than choosing between them.
198
+
199
+ The controller binds a Codex route to an exact host, App Server endpoint
200
+ generation, thread ID, and owner lease. Other provider-native IDs, PIDs,
201
+ working directories, endpoint paths, and socket paths are never public
202
+ selectors or output fields. A Claude UUID may be supplied explicitly as a
203
+ destination, but the gateway never prints or invents one for the caller.
204
+
205
+ Codex registration is explicit. A task registers its own alias and
206
+ authoritative `CODEX_THREAD_ID`; the gateway does not enumerate global Codex
207
+ history to invent routes. A route becomes usable only after the matching host
208
+ connector positively observes that exact task on the current endpoint
209
+ generation.
210
+
211
+ Claude discovery is passive and limited to currently advertised genuine
212
+ Claude session records. A validated native record whose current name begins
213
+ with reserved `codex-` is another gateway advertisement, not a selectable
214
+ Claude destination, and is excluded. Discovery produces a bounded, sanitized `availablePeers`
215
+ inventory keyed for display by the latest name. The adapter validates the
216
+ exact pinned schema, session UUID, process identity and liveness,
217
+ record/socket type, PID and socket-path correlation, allowed roots, and
218
+ file/socket generations. Provider-owned Unix owner and mode bits are not
219
+ treated as gateway policy; successful filesystem access is sufficient. A
220
+ current name resolves to a UUID but never substitutes for it.
221
+
222
+ A selected Claude UUID remains the durable route identity until explicit
223
+ unselection. Startup never enumerates Claude sessions and every restored route
224
+ begins stale. A later, separately authorized discovery operation may reactivate
225
+ the selection only when the full bounded scan contains exactly one compatible
226
+ interactive peer with the byte-identical UUID on the same provider, host, and
227
+ ownership lease. The adapter revalidates the current workspace and provider
228
+ selection before the store atomically adopts the current endpoint generation
229
+ and latest name. An incomplete scan, duplicate name or UUID, changed UUID,
230
+ workspace failure, or store collision leaves the route stale and releases any
231
+ provider selection acquired by the failed attempt. A name alone never restores
232
+ or retargets a durable selection.
233
+
234
+ The dashboard is the single pane for the human. It shows both sanitized
235
+ available/selected Claude aliases and explicitly registered Codex aliases,
236
+ including their host, compatibility, state, last-seen age, and queue depth.
237
+ The thin skill/CLI exposes the same safe alias list to either provider.
238
+
239
+ ## Message flows
240
+
241
+ ### Codex to Claude
242
+
243
+ 1. A registered Codex task calls the repo-shipped gateway skill/CLI with its own
244
+ thread identity, source alias, target Claude current name or session UUID,
245
+ and bounded text.
246
+ 2. The gateway checks thread ownership, selector state, rate and size limits,
247
+ deadline, hop count, and dedupe state.
248
+ 3. It requires the selector to match an explicitly selected live UUID,
249
+ refreshes the UUID's current process/socket coordinates, and revalidates
250
+ the selected Claude peer's canonical workspace access and exact generation
251
+ before every send.
252
+ 4. It opens a short-lived connection and writes one version-pinned peer frame.
253
+ In the designed write-enabled mode, a reply request carries the gateway's
254
+ own anonymous callback UDS as the reply address.
255
+ 5. It records only normalized delivery metadata. It does not retry an
256
+ ambiguous write automatically.
257
+ 6. A reply received on the callback listener is correlated in memory and
258
+ routed to the owning explicitly registered Codex task.
259
+
260
+ A transport write is not proof of successful model completion. The adapter
261
+ distinguishes transport state from any hold/release/denial receipt supported
262
+ by the pinned protocol. `transport_written` and native `held` are adapter/native
263
+ progress signals only, not public `delivery_status` states. The public tracker
264
+ remains `queued` or `stalled` until a terminal state; neither signal proves
265
+ terminal success or permits a retry.
266
+
267
+ ### Claude to Codex
268
+
269
+ This path is enabled for each explicitly registered `codex-*` task. The
270
+ gateway publishes a process-owned native registry entry per task, accepts
271
+ Claude's native `SendMessage`, starts an App Server turn, and returns the
272
+ final reply.
273
+
274
+ 1. The gateway advertises one process-owned `codex-*` record per registered
275
+ task in Claude's native registry. A supervised helper process owns each
276
+ advertisement's registry record and callback socket; the broker remains
277
+ the sole owner of state, queues, and dispatch.
278
+ 2. A real Claude session uses native `ListAgents` and `SendMessage`; the
279
+ gateway validates that exact live registry/socket generation and treats the
280
+ text as untrusted user-role input. This inbound observation grants only a
281
+ transient, in-memory capability for the correlated reply. It does not add a
282
+ Claude route, flip `selected`, or authorize a later unsolicited send.
283
+ In paired mode, a sender without the exact permission edge is refused before
284
+ message admission with `SENDER_NOT_PAIRED`; because no message was accepted,
285
+ that refusal is not added to the delivery journal.
286
+ 3. The Claude process's inherited messaging-socket value may be accepted as a
287
+ transient reply address after strict validation. Claude Code exports
288
+ `CLAUDE_CODE_MESSAGING_SOCKET` as a raw absolute socket path; the CLI
289
+ converts it in memory to the gateway's internal `uds:` capability. A user
290
+ never sets, prefixes, or passes that value manually. It is never logged,
291
+ persisted, rendered, or copied into normalized events.
292
+ 4. The gateway resolves the Codex alias to its private exact-thread binding.
293
+ 5. The resumed task retains its existing native permissions. The gateway does
294
+ not supply policy overrides.
295
+ 6. App Server status notifications atomically refresh the advertised native
296
+ peer record to `idle`, `busy`, or `waiting`.
297
+ 7. If the task is idle, the owning connector starts one dedicated turn.
298
+ Ordinary messages received while it is active or awaiting approval queue
299
+ internally. An exact leading `STEER:` body in this direction is marked as a
300
+ steering message. If the connector has a positively observed active turn
301
+ and no RPC already in flight, it sends the closed `turn/steer` request with
302
+ that exact ID as `expectedTurnId`; App Server admits the input at the next
303
+ tool-call boundary. Embassy never calls `turn/interrupt` for this path and
304
+ never injects text mid-generation. A clean non-steerable or unavailable
305
+ boundary silently returns the same body to the normal queue. It does not
306
+ emit Claude's approval-specific native `held` control frame for ordinary
307
+ queueing.
308
+ 8. In `merged` and `verbose` notice modes, if the delivery remains pending for
309
+ exactly `floor(messageDeadlineMs / 2)`, the gateway may send the
310
+ originating Claude session at most one nonterminal
311
+ `<gateway-delivery-stall>` user frame for that receipt. It contains only an
312
+ allowlisted reason and a bounded `queued-for-ms` age; it is not a native
313
+ `held` receipt and does not settle the delivery. `quiet` suppresses this
314
+ gateway-authored frame without changing native status or dashboard state.
315
+ 9. When the task becomes idle, the connector refreshes the exact task state and
316
+ starts the held message. A route retains at most three queued steering
317
+ messages; accepting a fourth atomically cancels the oldest with safe code
318
+ `STEER_QUEUE_SUPERSEDED`, a normal terminal receipt, and a `STEER`-marked
319
+ journal event. Explicit registration is sufficient authorization; Embassy
320
+ does not run an additional workspace or policy classifier.
321
+ 10. Successful App Server acceptance returns Claude's native `delivered`
322
+ receipt. A route or delivery error returns native `expired` with one safe
323
+ error code retained in its `reason` field. The default `merged` mode omits
324
+ the duplicate terminal user frame; `verbose` additionally sends a static
325
+ `<gateway-delivery-diagnostic>` user frame so the reason is readable in
326
+ Claude Code versions that do not render the native control reason. `quiet`
327
+ also omits gateway-authored stall frames. The diagnostic never contains a
328
+ socket path, session UUID, raw exception, or message body. `denied` is
329
+ reserved for an actual user or policy refusal.
330
+ A transient clean pre-dispatch failure returns the same message to the queue
331
+ instead of terminally failing it.
332
+ 11. Completion is summarized into bounded normalized state and the correlated
333
+ reply is returned only to the same originating Claude session UUID after
334
+ its current coordinates are uniquely re-resolved and revalidated.
335
+
336
+ The native receipt retains the originating Claude session's stable UUID, not
337
+ its mutable name, PID, registry record, or socket. Before every stall or
338
+ terminal receipt write, the adapter performs bounded discovery and revalidates
339
+ the UUID's current exact coordinates. This permits a receipt to follow ordinary
340
+ process/socket rotation without writing to a stale generation. If the UUID is
341
+ not uniquely and compatibly re-observed, the write fails closed. A terminal
342
+ write whose outcome is ambiguous is never replayed; only a proven pre-write
343
+ failure may be retried while the bounded in-memory receipt remains live. The
344
+ receipt correlation does not add the UUID or receipt handle to public output
345
+ or durable state; a separately selected route may already persist that same
346
+ Claude UUID as its private native route handle.
347
+
348
+ Delivery callback arrival is timestamped at the service boundary. A terminal
349
+ callback observed strictly before its message deadline is applied before the
350
+ deadline sweep even when event-loop scheduling delays its worker; a callback
351
+ observed at or after the exact deadline cannot reopen the expired attempt.
352
+ Shutdown is likewise two-phase: provider ingress is first quiesced so no new
353
+ user-message callback can enter and every already admitted callback completes,
354
+ while receipt writes remain available. The service then drains callbacks,
355
+ terminally settles accepted work, joins its bounded receipt writes, and only
356
+ then closes provider adapters. This orders `GATEWAY_SHUTDOWN` receipts ahead of
357
+ listener teardown instead of silently dropping late admitted work.
358
+
359
+ Claude's native peer socket is itself an inbox, so Codex replies may be
360
+ written while the Claude route is busy. The gateway still serializes its own
361
+ writes, but it does not wait for Claude to become idle and thereby deadlock a
362
+ Claude turn that is waiting for the reply.
363
+
364
+ The gateway exposes `turn/steer` only through an exact leading `STEER:` body in
365
+ the Claude-to-Codex direction. The global `EMBASSY_STEERING_ENABLED` switch is
366
+ on by default and exact `0` disables classification. The pinned 0.147.0 schema
367
+ requires `expectedTurnId`, rejects a nonmatching active turn, reports a clean
368
+ `activeTurnNotSteerable` condition, and returns the accepted turn ID. Embassy
369
+ validates all of those temporal correlations before settlement. `turn/interrupt`
370
+ is permitted only for a turn that the same connector started and positively
371
+ observed; steering never authorizes it, and there is no generic App Server RPC
372
+ escape hatch.
373
+
374
+ ### Delivery status and bounded waits
375
+
376
+ Every accepted control-plane `send_to_claude`, `send_to_codex`, or `reply`
377
+ result contains both its conversation ID and a fresh opaque, memory-only
378
+ delivery correlation handle called a delivery token.
379
+ The token has the closed form `dlv_` followed by exactly 24 base64url
380
+ characters (`A-Z`, `a-z`, `0-9`, `_`, or `-`). It addresses one bounded
381
+ in-memory delivery tracker and is not a provider receipt handle or a provider
382
+ native identifier.
383
+
384
+ The read-only `delivery_status` method accepts only that token and returns one
385
+ of these closed results:
386
+
387
+ - `{ found: false }`; or
388
+ - `{ found: true, state, terminal, updatedAt, deadlineAt, ... }`, where `state`
389
+ is one of `queued`, `stalled`, `delivered`, `unconfirmed`, `expired`,
390
+ `failed`, `ambiguous`, or `cancelled`. `terminal` is false exactly for
391
+ `queued` and `stalled`, and true for every other state. `pendingForMs` may
392
+ report the nonnegative age since gateway acceptance, including time spent in
393
+ flight, and `safeErrorCode` may report one shape-constrained broker code.
394
+
395
+ `updatedAt` and `deadlineAt` are ISO timestamps. A terminal result guarantees
396
+ only that this gateway delivery attempt will not transition again. It does not
397
+ guarantee a model reply or make an ambiguous outcome safe to retry. A stalled
398
+ result is progress only, even after the one sender-visible stall notice when
399
+ the configured notice policy permits it.
400
+
401
+ The CLI exposes `delivery-status --token <token>` for one read and
402
+ `wait-delivery --token <token>` for a bounded wait. The waiter uses the same
403
+ read-only method every 250 ms, emits only the terminal result, and stops no
404
+ later than the delivery deadline plus the control client's 3-second allowance.
405
+ An unknown token fails immediately. A wait timeout is not a terminal delivery
406
+ state and does not authorize a resend.
407
+
408
+ `unconfirmed` and `ambiguous` are distinct terminal outcomes. `unconfirmed`
409
+ means the transport write itself was confirmed but terminal provider evidence
410
+ was never observed; `ambiguous` means the write outcome is unknown. Both are
411
+ terminal, neither is a retry authorization, and both exit `6`.
412
+
413
+ `wait-delivery` exits `0` only for `delivered`. It exits `6` for every other
414
+ terminal state (`unconfirmed`, `expired`, `failed`, `ambiguous`, or
415
+ `cancelled`) while preserving the exact terminal result in its JSON output. An
416
+ unknown token exits `3`; a local bounded-wait timeout exits `4` and is not a
417
+ terminal state.
418
+
419
+ The status table is bounded. Under capacity pressure Embassy evicts only the
420
+ oldest terminal correlation handle; active `queued` or `stalled` handles are
421
+ never displaced to admit a new send. A pressure-evicted handle returns
422
+ `{ found: false }`, just like a handle whose retention window elapsed.
423
+
424
+ ### Replies and process restarts
425
+
426
+ Conversation IDs correlate replies, but callback addresses and message bodies
427
+ exist only in memory. After a gateway restart, previously queued or in-flight
428
+ metadata is marked abandoned; bodies are not recoverable and are never
429
+ replayed. The prior Claude binding remains stored but stale. After authorized
430
+ live discovery, one exact UUID-bound selection may be reactivated under its
431
+ latest name. No queued text, pending reply, callback, native receipt handle,
432
+ delivery token/status tracker, or conversation capability survives the
433
+ restart; a prior token therefore returns `found: false`. A stale or offline
434
+ selection can be explicitly removed by its stored alias or a user-supplied UUID
435
+ without requiring discovery first.
436
+
437
+ ## Gateway control plane
438
+
439
+ The control plane is newline-delimited JSON on one Unix-domain socket inside a
440
+ controller-owned mode-0700 state directory. The socket and state files are
441
+ mode 0600. Frames are size-bounded and closed against unknown keys, methods,
442
+ versions, and enum values.
443
+
444
+ The small version 1 method family covers:
445
+
446
+ - health and a safe public snapshot;
447
+ - a read-only `observe_snapshot` projection, which may settle already-due
448
+ lifecycle deliveries before projecting and is the only method the live
449
+ dashboard companion calls;
450
+ - explicit Codex registration, succession, and unregister;
451
+ - explicit Claude selection and unselection from the current sanitized
452
+ available-peer inventory;
453
+ - delivery-status lookup by an opaque, memory-only correlation handle;
454
+ - provider-specific send operations;
455
+ - a correlated reply operation; and
456
+ - dashboard refresh.
457
+
458
+ The installed binary is `embassy` (`claude-codex-gateway` is a one-release
459
+ deprecated alias). Its implemented commands are
460
+ `serve`, `health`, `status`, `delivery-status`, `wait-delivery`,
461
+ `refresh-dashboard`, `dashboard`, `register-codex`, `unregister-codex`,
462
+ `select-claude`, `unselect-claude`, `send-to-claude`, `send-to-codex`, and
463
+ `reply`. `dashboard` requires `--live` and accepts an optional
464
+ `--lang en|zh-CN`; it starts the companion process rather than issuing a single
465
+ control request. Message bodies are non-empty UTF-8 from standard input only,
466
+ with a 16 KiB ceiling; they are never accepted in an argument or file. The
467
+ client emits one bounded normalized JSON line and never returns a thread ID,
468
+ provider-native ID, path, address, or message body. These commands require the
469
+ foreground broker, except that `serve` starts it in the current terminal. It
470
+ never daemonizes itself.
471
+
472
+ `select-claude --alias <current-name@host>` and
473
+ `select-claude --session <uuid>` select the same logical session.
474
+ `send-to-claude --to` accepts either form only after explicit selection. UUID
475
+ input is normalized to lowercase. No command returns the
476
+ UUID, and no historical name remains routable after a rename.
477
+
478
+ Provider-authorized mutations require one exclusive inherited principal.
479
+ Codex registration, unregister, and Codex-to-Claude send require only a valid
480
+ `CODEX_THREAD_ID`; they fail if a non-empty Claude messaging socket is also
481
+ inherited. Claude-to-Codex send requires only the raw inherited Claude socket
482
+ path and fails if a non-empty Codex thread ID is also present. `reply` likewise
483
+ fails with both identities or neither. The operator-only health, status,
484
+ dashboard refresh, live dashboard, select, unselect, and serve commands ignore
485
+ provider identities.
486
+
487
+ The foreground command is:
488
+
489
+ ```text
490
+ embassy serve
491
+ ```
492
+
493
+ Before provider attestation, listener creation, or App Server attachment, the
494
+ launcher acquires one fixed host-wide crash-reclaimable owner lease under the
495
+ verified login home. The lease is independent of `EMBASSY_STATE_DIR`, so two foreground
496
+ controllers cannot be started for the same login account by choosing different
497
+ state roots. During the one-release prototype transition, a recognized exact
498
+ legacy default root is also controller-locked while Embassy runs. Any
499
+ pre-existing legacy lock blocks startup as `GATEWAY_INSTANCE_IN_USE` and is
500
+ preserved; the operator may remove that exact stale lock only after confirming
501
+ no prototype process remains. Missing, unsafe, or unrecognized legacy roots
502
+ are not created, imported, or mutated.
503
+
504
+ It emits one normalized ready line, publishes the private dashboard, and
505
+ holds the process until `SIGINT` or `SIGTERM`, when exact-owned resources are
506
+ closed. Startup attests the pinned local Claude and Codex runtimes and binds
507
+ controller-owned UDS listeners, but does not discover a Claude peer, write a
508
+ provider socket, start a model turn, or contact a remote host. Its ready result
509
+ reports local host `this-mac`, dashboard filename `gateway-dashboard.html`, and
510
+ `codexMode: "native_messaging"` without exposing paths.
511
+
512
+ There is no arbitrary filesystem operation, shell command, SSH command, App
513
+ Server method, Claude registry mutation, credential argument, approval reply,
514
+ or raw diagnostic method.
515
+
516
+ Same-UID socket access is a local containment boundary, not proof of a trusted
517
+ agent process. Every mutation additionally checks route ownership, exact
518
+ thread/session generation, source alias, bounds, and conversation state.
519
+
520
+ ## Codex connectors and remote hosts
521
+
522
+ In the target multi-host design, each allowlisted execution host has a separate
523
+ connector because each host has its own App Server and native state:
524
+
525
+ - `this-mac`: the managed local App Server shared with Desktop;
526
+ - `build-mac`: a host-local remote App Server reached through an attach-only SSH
527
+ proxy; and
528
+ - `lab-mac.example`: the same design, still unprobed and disabled by default.
529
+
530
+ The shipped foreground launcher accepts only `this-mac`; it rejects any remote
531
+ host configuration. The two SSH connectors above remain planned rather than
532
+ runnable v1 routes.
533
+
534
+ The local connector resolves the managed standalone Codex release by exact
535
+ path and version; it does not use `PATH`. That installation is separate from
536
+ any NVM-managed `codex` on the user's `PATH` (for example
537
+ `~/.nvm/versions/node/*/bin/codex`), does not replace
538
+ it, and does not edit a shell profile. The two installations therefore do not
539
+ conflict.
540
+
541
+ The connector has a fixed App Server method allowlist. It may initialize,
542
+ observe loaded tasks, resume/unsubscribe the exact registered task, start a
543
+ dedicated turn, and interrupt only its own confirmed turn. Archive, delete,
544
+ history, shell, configuration, authentication, plugin, approval-response, and
545
+ generic RPC methods are excluded.
546
+
547
+ Exact App Server 0.147.0 gates the privacy-preserving
548
+ `thread/resume.excludeTurns` field behind initialization capability
549
+ `experimentalApi: true`. The connector therefore hard-codes that one
550
+ non-configurable capability solely to suppress history retrieval. Both initial
551
+ resume and the immediate pre-start refresh send exactly `threadId` plus
552
+ `excludeTurns: true`, then require an exact empty `thread.turns` array.
553
+ Missing, malformed, or nonempty turns fail closed and are never emitted or
554
+ persisted. The capability does not add an experimental client method or change
555
+ the closed RPC allowlist.
556
+
557
+ Monitor compatibility and write compatibility are distinct gates. A connector
558
+ may initialize, list, resume, and expose normalized monitor state after its
559
+ schema compatibility is attested while still reporting its write gate as
560
+ unavailable. No Claude-initiated turn can start until exact write compatibility
561
+ and explicit route ownership are established.
562
+
563
+ Registration resumes the exact task and establishes reachability. Embassy does
564
+ not read or retain reported working-directory or policy fields. Before
565
+ `turn/start`, including a queued drain, the connector refreshes that exact task
566
+ on the same live connection, requires it to be idle, and starts the turn with
567
+ no policy overrides. Settings notifications cannot make an explicitly
568
+ registered live route unreachable or discard its accepted queue.
569
+
570
+ Version 1 never changes or independently classifies a Codex task's approval or
571
+ sandbox policy. Offline 0.147.0 `TurnStartParams` schema evidence shows that
572
+ policy overrides persist for the current and subsequent turns, so using them
573
+ as per-message restrictions would silently mutate the native task. Embassy
574
+ therefore starts the turn without overrides and leaves approval, sandbox, and
575
+ tool enforcement to the registered task's native Codex configuration. Explicit
576
+ `codex-*` registration plus exact live thread/generation validation is the
577
+ gateway reachability boundary; native task policy remains Codex's concern.
578
+
579
+ A remote connector never starts, stops, replaces, signals, or unlinks a
580
+ Desktop-owned App Server or its socket. If attach fails, the host is offline;
581
+ Desktop remains responsible for lifecycle recovery. SSH aliases are fixed
582
+ operator configuration, never model-provided strings. Normal OpenSSH host-key
583
+ validation applies.
584
+
585
+ ### Completed no-model feasibility evidence
586
+
587
+ On 2026-08-07:
588
+
589
+ - A no-model environment check in the current Codex task confirmed that the
590
+ task tool process inherits `CODEX_THREAD_ID` and that its value matches the
591
+ required UUID grammar. The check emitted booleans only and never printed or
592
+ retained the identifier. This validates the repo skill/CLI premise that a
593
+ Codex task can self-register without accepting its private thread ID as a
594
+ command-line argument.
595
+ - A local attach-only probe connected through a second proxy to managed Codex
596
+ App Server 0.147.0, initialized, called only `thread/loaded/list`, and
597
+ confirmed the current task was already loaded. It emitted normalized
598
+ booleans and an aggregate count, then confirmed cleanup of only its own
599
+ proxy process.
600
+ - An authorized remote probe attached through a second SSH proxy to the
601
+ already-running `build-mac` App Server (remote Codex CLI 0.145.0), initialized,
602
+ and validated a schema-correct `thread/loaded/list`. It printed no task IDs,
603
+ payloads, remote diagnostics, history, or credentials and left Desktop's
604
+ original proxy alive.
605
+
606
+ Both proxy processes required their exact-owned forced-cleanup fallback after
607
+ the bounded graceful-close window; final cleanup was confirmed. These probes
608
+ prove attach and loaded-task discovery on the tested versions. They do not
609
+ prove notification fanout, approval routing, or writable task control.
610
+
611
+ The connector requires turn notifications to carry the exact `threadId` and
612
+ correlates the exact `turn.id`; `item/completed` must carry the exact
613
+ top-level `threadId` and `turnId`. Public protocol examples do not establish
614
+ every live notification field. Isolated no-model schema generation from
615
+ managed Codex App Server 0.147.0 now confirms that its v2 `TurnStarted` and
616
+ `TurnCompleted` notifications require `threadId` plus `turn`, and
617
+ `ItemCompleted` requires `threadId`, `turnId`, and `item`; an
618
+ `agentMessage` item includes `id`, `text`, and `type`. This clears the
619
+ correlation-shape question for exact version 0.147.0 without connecting to App
620
+ Server or a provider. Live multi-client notification fanout and writable
621
+ behavior remain untested, and any runtime mismatch still fails closed.
622
+
623
+ The same offline 0.147.0 schema generation confirms that
624
+ `TurnStartParams.approvalPolicy` and `sandboxPolicy` are persisted for the
625
+ current and subsequent turns. That no-model evidence is why version 1 sends no
626
+ seemingly temporary policy override.
627
+
628
+ Offline 0.147.0 schema generation also confirms that `TurnSteerParams` requires
629
+ exact `threadId`, `input`, and `expectedTurnId`; the precondition fails
630
+ when that ID is not the current active turn. `TurnSteerResponse` returns the
631
+ accepted `turnId`, and the closed App Server error shape includes
632
+ `activeTurnNotSteerable`. Embassy pins and validates this schema, delegates the
633
+ next-tool-call timing boundary to App Server, treats a clean refusal as normal
634
+ queue fallback, and treats malformed or write-ambiguous results as terminally
635
+ uncertain without replay.
636
+
637
+ The one Desktop restart needed for the local shared-App-Server feasibility
638
+ test has already been completed. Building, running synthetic tests, starting
639
+ the gateway, rendering the dashboard, and a future Claude peer-socket test do
640
+ not themselves require another Desktop restart. A provider or Desktop upgrade
641
+ may require a new compatibility probe and, if its attachment mode changes, a
642
+ separately announced controlled restart.
643
+
644
+ ## Dashboard
645
+
646
+ Version 1 generates self-contained HTML files under the controller-owned state
647
+ directory: `gateway-dashboard.html` and `gateway-dashboard.zh-CN.html`, both
648
+ rendered from one typed catalog and both atomically replaced, mode 0600, on
649
+ every publish. Each page links to the other; that in-page link is the only
650
+ static language switch, and `refresh-dashboard` takes no `--lang`. Each page
651
+ has inline CSS and a restrictive Content Security Policy, with no JavaScript,
652
+ external assets, CDN, cookies, local storage, service worker, telemetry,
653
+ mutation endpoint, or network listener.
654
+
655
+ A static page is a point-in-time snapshot and never refreshes itself: it emits
656
+ no meta refresh and the page tells the operator to re-run
657
+ `embassy refresh-dashboard` and reload, or to use `embassy dashboard --live`.
658
+
659
+ It shows only:
660
+
661
+ - aggregate gateway and per-host connector health;
662
+ - available/selected Claude aliases, registered Codex aliases, provider, host,
663
+ compatibility, state, and queue depth;
664
+ - normalized message direction and delivery state, timestamp, latency, byte
665
+ count, hop count, and a short opaque message-ID suffix; and
666
+ - allowlisted alerts such as stale route, protocol mismatch, queue full, or
667
+ ambiguous delivery.
668
+
669
+ It never shows message content, prompts, replies, transcripts, titles, working
670
+ directories, native IDs, PIDs, socket paths, endpoint paths, tool data, raw
671
+ events, stderr, credentials, or configuration contents. This is a
672
+ controller-owned UI artifact, not a shared task file. The public snapshot has
673
+ a 240 KiB projection budget and reports explicit omission counters if bounded
674
+ connector, peer, route, message, or alert rows are truncated.
675
+
676
+ ### Live dashboard companion
677
+
678
+ `embassy dashboard --live` is the opt-in browser view of the same projection.
679
+ It is a separate foreground process, not a mode of `embassy serve`: it holds no
680
+ provider capability, owns no registry record, and reaches the broker over the
681
+ same private control socket every other client command uses, so it reports the
682
+ gateway as unavailable when nothing is serving.
683
+
684
+ - **Bind.** One `http.createServer` listener on `127.0.0.1` with an ephemeral
685
+ port, under a random per-run instance path. No other interface is bound and
686
+ no port is fixed or advertised.
687
+ - **Bootstrap.** Startup mints one 256-bit (32-byte) capability, base64url
688
+ encoded, carried only in the URL fragment. The bootstrap URL is written to a
689
+ mode-0600 `bootstrap.html` inside a fresh mode-0700 `live-<random>` run
690
+ directory under the private state root, opened `wx` and identity-checked, and
691
+ both file and directory are removed when the companion exits.
692
+ - **Exchange.** The capability is single-use. It is exchanged once for a
693
+ path-scoped `HttpOnly` `SameSite=Strict` session cookie; the fragment never
694
+ reaches the server as part of a request line.
695
+ - **Request checks.** The exact Host header is validated on every request.
696
+ Navigation GETs may omit Origin and carry no sentinel; non-navigation POSTs
697
+ require the exact Origin plus `X-Embassy-Request`. There are no CORS headers,
698
+ no cross-origin reads, and no routes outside the instance path.
699
+ - **Projection and actions.** The companion observes through
700
+ `observe_snapshot`. Its only mutations are exact two-endpoint `pair`,
701
+ `unpair`, and `refresh_dashboard` control calls behind one closed
702
+ authenticated `/action` route. The browser shows the consequence and requires
703
+ explicit confirmation; the server rejects bodies over 1 KiB and limits the
704
+ companion to six actions per minute. It cannot register, unregister, succeed,
705
+ send, reply, approve, interrupt, change settings, or invoke a generic/provider
706
+ method. Each mutation touches only the edge it names: adding an edge never
707
+ retires another, and removing one settles its accepted work before the
708
+ change is published. Every action is followed by a
709
+ fresh observation. An observation may
710
+ settle already-due lifecycle deliveries before projecting, which is a broker
711
+ timer effect, not additional browser authority.
712
+ - **Containment.** Authentication scopes the browser, not the machine. Any
713
+ process running as the same OS user — including root and browser extensions
714
+ with local filesystem access — can read what the browser can read.
715
+
716
+ `--lang en|zh-CN` selects the companion's display language. It has no effect on
717
+ the static pair, which is always written in both languages.
718
+
719
+ ## Persistence and privacy
720
+
721
+ The private store may retain:
722
+
723
+ - aliases, enabled state, ownership leases, and exact provider-native route
724
+ handles inside the closed controller-private binding schema;
725
+ - endpoint-generation and compatibility markers;
726
+ - bounded durable queue-ledger and normalized delivery metadata used for
727
+ accounting and dashboard projection;
728
+ - timestamps, counters, dedupe/rate-limit records, and safe error codes.
729
+
730
+ It must never retain message bodies, provider output, prompts, replies, tool
731
+ input/output, raw App Server or Claude frames, stderr, histories, credentials,
732
+ Claude registry payloads, or callback/socket paths. The public snapshot is a
733
+ strict projection that also removes private route handles and endpoint
734
+ generations. The state directory is mode 0700 and binding state is mode 0600;
735
+ provider-native identifiers never enter normalized events, public snapshots,
736
+ the dashboard, CLI arguments/output, aliases, logs, or error text. On restart,
737
+ every restored route begins stale and unusable. An authorized discovery may
738
+ reactivate only the byte-identical durable Claude UUID after the current
739
+ provider endpoint, workspace, complete unique discovery, and ownership lease
740
+ all revalidate. The public `selected` bit flips only after that atomic private
741
+ rebind succeeds. No queued body, callback, receipt handle, conversation, or
742
+ reply capability is restored, and no delivery token or status tracker is
743
+ reconstructed.
744
+
745
+ The delivery-token mapping, queryable status tracker, native receipt handle,
746
+ and one-stall-notice state are always memory-only. Durable delivery metadata
747
+ does not contain enough information to reconstruct any of those capabilities
748
+ or replay a body after restart.
749
+
750
+ ## Minimum filesystem and process access
751
+
752
+ The production gateway does not need broad home-directory access or the
753
+ user's interactive Claude history. The narrow live boundary is:
754
+
755
+ - read/enumerate only the exact Claude live-session registry directory;
756
+ - stat/connect only validated peer sockets inside the exact Claude socket
757
+ directory;
758
+ - create and later remove only its exact-owned callback socket inside the
759
+ accessible Claude peer-socket directory, with inode/generation checks;
760
+ - create its control socket plus metadata/dashboard files only inside its
761
+ separate controller-owned mode-0700 state directory;
762
+ - attach to explicitly allowlisted Codex App Server endpoints; and
763
+ - optionally execute fixed `ssh`/Codex proxy argv for allowlisted hosts, with
764
+ no shell and no model-supplied command or hostname.
765
+
766
+ It does not read Claude transcripts, settings, project state, credentials,
767
+ Keychain, shell history, or unrelated user files. It does not copy, print,
768
+ persist, or manipulate authentication material. Routine tests replace all of
769
+ the boundaries above with test-owned temporary directories, fake UDS peers,
770
+ and fake App Server transports.
771
+
772
+ ### Exact default roots on macOS
773
+
774
+ The runtime attestation code derives these paths from the current OS user's
775
+ verified home; it does not scan the home directory. These are the reviewed
776
+ boundaries exercised by the live gateway; routine tests substitute synthetic
777
+ paths, peers, and transports:
778
+
779
+ | Path/capability | Minimum purpose |
780
+ | --- | --- |
781
+ | `~/.local/bin/claude` and derived expected target `~/.local/share/claude/versions/2.1.226` | Stat the owned launcher/path components and read/execute only the resolved pinned target for bounded `--version`; live launcher attestation succeeded |
782
+ | `~/.claude/sessions` | Read/enumerate only live registry JSON during the separately authorized passive-discovery gate |
783
+ | `/tmp/cc-socks` | At foreground startup, validate the private directory and create/remove only `/tmp/cc-socks/<gateway-pid>.sock` after inode/generation checks; search/stat genuine peers at passive discovery and connect one validated target only at the separately authorized send gate |
784
+ | `~/.local/state/agent-embassy/.agent-embassy-state` | Validate or establish the exact ownership marker before creating the fixed host lease; an existing non-empty unmarked root is rejected without mutation |
785
+ | `/usr/bin/lockf` and `/bin/cat` | Hold one fixed, non-waiting macOS advisory lease for the foreground controller; the helper receives no shell text, provider data, or model-supplied argument |
786
+ | `~/.local/state/agent-embassy/.gateway-host.lock` | Fixed per-login kernel-held lease acquired before provider setup; it remains here even when `EMBASSY_STATE_DIR` is overridden. Its bounded PID/token record is exact-cleanup metadata, not a path-only stale-lock authority; a crash releases the kernel lock and the next foreground process may acquire the existing file |
787
+ | `~/.local/state/claude-agent-bridge/gateway/.claude-codex-gateway-state` and `.gateway-controller.lock` | For one release, bounded-read the exact legacy ownership marker and lock record; create and hold the lock only when absent, preserve any pre-existing lock, and read no other legacy state or message data |
788
+ | `~/.local/state/agent-embassy` (or explicit `EMBASSY_STATE_DIR`) | Default controller-owned store, control UDS, state lock, and static dashboard; an explicit absolute configuration may replace only these state surfaces |
789
+ | `~/.codex/packages/standalone` and `~/.codex/app-server-control/app-server-control.sock` | Resolve the pinned managed Codex binary and attach to the already-running private local App Server; never bootstrap or unlink it |
790
+
791
+ No grant to `~/.claude/projects`, the rest of
792
+ `~/.claude`, Keychain APIs, the full home directory, or
793
+ unrelated temporary files is required. Remote-host access is a later,
794
+ separately reviewed fixed-SSH-alias capability.
795
+
796
+ Selected Claude workspaces may contain the private controller-state directory. The
797
+ filesystem root and configured temporary roots are still rejected as
798
+ deliberately broad Claude workspaces. The user's home is selectable with the
799
+ default controller-state root beneath it. A narrower project directory remains
800
+ the preferred least-context setup, but it is not mandatory.
801
+
802
+ ## Failure and upgrade policy
803
+
804
+ - Unknown Claude Code version, peer protocol, message version, App Server
805
+ response shape, or endpoint generation fails closed.
806
+ - Alias collisions, stale ownership leases, PID/socket races, unsafe
807
+ gateway-owned state, unexpected paths, queue overflow, deadline expiry, and ambiguous writes are
808
+ normalized failures, never raw diagnostics.
809
+ - Provider disconnect invalidates every route on that endpoint generation.
810
+ - Explicit `register-codex` replaces a closed or faulted App Server connector;
811
+ if the recovered route is idle, held undispatched work is woken. Ambiguous
812
+ writes are not retried.
813
+ - The first successful Codex registration locks its exact alias, task, and host
814
+ until that registration is explicitly succeeded. Exact re-registration and
815
+ connector recovery remain valid; unregister removes reachability but does not
816
+ by itself release the identity.
817
+ - `register-codex --alias <new> --succeeds <current>`, issued from inside the
818
+ successor task on the same host with its own inherited `CODEX_THREAD_ID`, is
819
+ the one path that changes the registered Codex identity without a restart. A
820
+ successor must name the exact active registration, on the same host, with a
821
+ different alias and a different thread; anything else is
822
+ `CODEX_SUCCESSION_OWNER_MISMATCH`. The journaled machine freezes the outgoing
823
+ generation's ingress and dispatch, drains callbacks and receipt writes, and
824
+ requires a clean quiescence barrier before it prepares anything durable.
825
+ Nothing transfers: conversations, pending reply capabilities, queued bodies,
826
+ and delivery tokens belong to the retired identity and are purged rather than
827
+ inherited, and the successor is published on a fresh listener generation.
828
+ Publication arming is the irreversible boundary — before it, a failure rolls
829
+ back to the old registration; at or after an armed, published, or unknown
830
+ observation, the old registration is never restored, and an incomplete
831
+ succession leaves registration offline and pinned until manual recovery
832
+ rather than leaving two live registrations.
833
+ - A failed reactivation of a retained route, or any fresh-registration rollback
834
+ whose cleanup cannot be fully confirmed, pins that exact identity
835
+ fail-closed. Only exact retry is permitted until the old route is confirmed
836
+ unregistered and the controller is restarted.
837
+ - No ambiguous mutation is retried automatically.
838
+ - No queued body survives process loss.
839
+ - Version-specific compatibility evidence expires on a provider or Desktop
840
+ update.
841
+
842
+ ## Validation boundary
843
+
844
+ Routine validation is deterministic and synthetic: it does not inspect live
845
+ provider state, connect a provider socket, attach to App Server, invoke SSH, or
846
+ make a model request. The separately authorized local live tests recorded
847
+ above established discovery and both message directions. Remote production
848
+ connectors remain a separately reviewed future capability.
849
+
850
+ Only the synthetic layer is routine validation. Server/dashboard startup,
851
+ discovery, and callback binding remain no-send operations; step 4 is the first
852
+ provider write. A real provider message is never enabled in CI.
853
+
854
+ ## References
855
+
856
+ - [Anthropic: Message your other Claude Code sessions](https://code.claude.com/docs/en/cross-session-messaging)
857
+ - [OpenAI: Codex App Server](https://learn.chatgpt.com/docs/app-server)
858
+ - [OpenAI: Remote connections and SSH hosts](https://learn.chatgpt.com/docs/remote-connections)