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.
- package/CHANGELOG.md +61 -0
- package/CONTRIBUTING.md +122 -0
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/README.zh-CN.md +185 -0
- package/SECURITY.md +224 -0
- package/assets/live-dashboard/app.css +1610 -0
- package/assets/mark-seal.svg +7 -0
- package/assets/mark.svg +7 -0
- package/assets/social-preview.png +0 -0
- package/assets/vendor/react/LICENSE +21 -0
- package/assets/vendor/react/react-dom.production.min.js +267 -0
- package/assets/vendor/react/react.production.min.js +31 -0
- package/dist/src/errors.d.ts +5 -0
- package/dist/src/errors.js +11 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/gateway/claude-compatibility-scratch.d.ts +18 -0
- package/dist/src/gateway/claude-compatibility-scratch.js +158 -0
- package/dist/src/gateway/claude-compatibility-scratch.js.map +1 -0
- package/dist/src/gateway/claude-helper-client.d.ts +43 -0
- package/dist/src/gateway/claude-helper-client.js +212 -0
- package/dist/src/gateway/claude-helper-client.js.map +1 -0
- package/dist/src/gateway/claude-helper-protocol.d.ts +172 -0
- package/dist/src/gateway/claude-helper-protocol.js +373 -0
- package/dist/src/gateway/claude-helper-protocol.js.map +1 -0
- package/dist/src/gateway/claude-helper-supervisor.d.ts +74 -0
- package/dist/src/gateway/claude-helper-supervisor.js +497 -0
- package/dist/src/gateway/claude-helper-supervisor.js.map +1 -0
- package/dist/src/gateway/claude-helper.d.ts +1 -0
- package/dist/src/gateway/claude-helper.js +269 -0
- package/dist/src/gateway/claude-helper.js.map +1 -0
- package/dist/src/gateway/claude-peer.d.ts +273 -0
- package/dist/src/gateway/claude-peer.js +2359 -0
- package/dist/src/gateway/claude-peer.js.map +1 -0
- package/dist/src/gateway/claude-runtime.d.ts +40 -0
- package/dist/src/gateway/claude-runtime.js +302 -0
- package/dist/src/gateway/claude-runtime.js.map +1 -0
- package/dist/src/gateway/cli-copy.d.ts +8 -0
- package/dist/src/gateway/cli-copy.en.d.ts +13 -0
- package/dist/src/gateway/cli-copy.en.js +43 -0
- package/dist/src/gateway/cli-copy.en.js.map +1 -0
- package/dist/src/gateway/cli-copy.js +19 -0
- package/dist/src/gateway/cli-copy.js.map +1 -0
- package/dist/src/gateway/cli-copy.zh-CN.d.ts +13 -0
- package/dist/src/gateway/cli-copy.zh-CN.js +43 -0
- package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -0
- package/dist/src/gateway/cli.d.ts +44 -0
- package/dist/src/gateway/cli.js +969 -0
- package/dist/src/gateway/cli.js.map +1 -0
- package/dist/src/gateway/codex-app-server.d.ts +310 -0
- package/dist/src/gateway/codex-app-server.js +1746 -0
- package/dist/src/gateway/codex-app-server.js.map +1 -0
- package/dist/src/gateway/codex-local-transport.d.ts +81 -0
- package/dist/src/gateway/codex-local-transport.js +617 -0
- package/dist/src/gateway/codex-local-transport.js.map +1 -0
- package/dist/src/gateway/codex-registration-generation.d.ts +5 -0
- package/dist/src/gateway/codex-registration-generation.js +19 -0
- package/dist/src/gateway/codex-registration-generation.js.map +1 -0
- package/dist/src/gateway/codex-registration-succession.d.ts +209 -0
- package/dist/src/gateway/codex-registration-succession.js +588 -0
- package/dist/src/gateway/codex-registration-succession.js.map +1 -0
- package/dist/src/gateway/compatibility.d.ts +70 -0
- package/dist/src/gateway/compatibility.js +252 -0
- package/dist/src/gateway/compatibility.js.map +1 -0
- package/dist/src/gateway/config.d.ts +24 -0
- package/dist/src/gateway/config.js +130 -0
- package/dist/src/gateway/config.js.map +1 -0
- package/dist/src/gateway/control.d.ts +308 -0
- package/dist/src/gateway/control.js +1573 -0
- package/dist/src/gateway/control.js.map +1 -0
- package/dist/src/gateway/dashboard-copy.d.ts +7 -0
- package/dist/src/gateway/dashboard-copy.en.d.ts +506 -0
- package/dist/src/gateway/dashboard-copy.en.js +507 -0
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -0
- package/dist/src/gateway/dashboard-copy.js +516 -0
- package/dist/src/gateway/dashboard-copy.js.map +1 -0
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +506 -0
- package/dist/src/gateway/dashboard-copy.zh-CN.js +507 -0
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -0
- package/dist/src/gateway/dashboard-model.d.ts +207 -0
- package/dist/src/gateway/dashboard-model.js +720 -0
- package/dist/src/gateway/dashboard-model.js.map +1 -0
- package/dist/src/gateway/dashboard.d.ts +26 -0
- package/dist/src/gateway/dashboard.js +941 -0
- package/dist/src/gateway/dashboard.js.map +1 -0
- package/dist/src/gateway/delivery-machine.d.ts +235 -0
- package/dist/src/gateway/delivery-machine.js +540 -0
- package/dist/src/gateway/delivery-machine.js.map +1 -0
- package/dist/src/gateway/instance-lease.d.ts +21 -0
- package/dist/src/gateway/instance-lease.js +549 -0
- package/dist/src/gateway/instance-lease.js.map +1 -0
- package/dist/src/gateway/live-dashboard-app/app.js +2914 -0
- package/dist/src/gateway/live-dashboard-assets.d.ts +10 -0
- package/dist/src/gateway/live-dashboard-assets.js +73 -0
- package/dist/src/gateway/live-dashboard-assets.js.map +1 -0
- package/dist/src/gateway/live-dashboard-bootstrap.d.ts +36 -0
- package/dist/src/gateway/live-dashboard-bootstrap.js +176 -0
- package/dist/src/gateway/live-dashboard-bootstrap.js.map +1 -0
- package/dist/src/gateway/live-dashboard-command.d.ts +54 -0
- package/dist/src/gateway/live-dashboard-command.js +324 -0
- package/dist/src/gateway/live-dashboard-command.js.map +1 -0
- package/dist/src/gateway/live-dashboard-http.d.ts +41 -0
- package/dist/src/gateway/live-dashboard-http.js +445 -0
- package/dist/src/gateway/live-dashboard-http.js.map +1 -0
- package/dist/src/gateway/live-dashboard-protocol.d.ts +38 -0
- package/dist/src/gateway/live-dashboard-protocol.js +156 -0
- package/dist/src/gateway/live-dashboard-protocol.js.map +1 -0
- package/dist/src/gateway/live-dashboard-server.d.ts +33 -0
- package/dist/src/gateway/live-dashboard-server.js +130 -0
- package/dist/src/gateway/live-dashboard-server.js.map +1 -0
- package/dist/src/gateway/live-dashboard-stream.d.ts +46 -0
- package/dist/src/gateway/live-dashboard-stream.js +233 -0
- package/dist/src/gateway/live-dashboard-stream.js.map +1 -0
- package/dist/src/gateway/live-dashboard.d.ts +31 -0
- package/dist/src/gateway/live-dashboard.js +190 -0
- package/dist/src/gateway/live-dashboard.js.map +1 -0
- package/dist/src/gateway/locale.d.ts +4 -0
- package/dist/src/gateway/locale.js +10 -0
- package/dist/src/gateway/locale.js.map +1 -0
- package/dist/src/gateway/progress-watch-machine.d.ts +97 -0
- package/dist/src/gateway/progress-watch-machine.js +172 -0
- package/dist/src/gateway/progress-watch-machine.js.map +1 -0
- package/dist/src/gateway/providers.d.ts +287 -0
- package/dist/src/gateway/providers.js +2451 -0
- package/dist/src/gateway/providers.js.map +1 -0
- package/dist/src/gateway/server.d.ts +57 -0
- package/dist/src/gateway/server.js +264 -0
- package/dist/src/gateway/server.js.map +1 -0
- package/dist/src/gateway/service.d.ts +449 -0
- package/dist/src/gateway/service.js +4831 -0
- package/dist/src/gateway/service.js.map +1 -0
- package/dist/src/gateway/store.d.ts +340 -0
- package/dist/src/gateway/store.js +4047 -0
- package/dist/src/gateway/store.js.map +1 -0
- package/dist/src/gateway/types.d.ts +514 -0
- package/dist/src/gateway/types.js +414 -0
- package/dist/src/gateway/types.js.map +1 -0
- package/dist/src/mutex.d.ts +4 -0
- package/dist/src/mutex.js +23 -0
- package/dist/src/mutex.js.map +1 -0
- package/docs/DESIGN.md +134 -0
- package/docs/GATEWAY-ARCHITECTURE.md +858 -0
- package/package.json +80 -0
- package/skills/embassy-peer/SKILL.md +198 -0
- 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)
|