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,969 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Local gateway foreground launcher and metadata-only control client.
|
|
4
|
+
*
|
|
5
|
+
* Provider identities and message bodies are accepted only from inherited
|
|
6
|
+
* process state and stdin respectively. They are never reflected in output or
|
|
7
|
+
* error text. Only `serve` owns the long-lived provider capabilities and
|
|
8
|
+
* private control server; every other command is a bounded client operation.
|
|
9
|
+
*/
|
|
10
|
+
import { realpathSync } from "node:fs";
|
|
11
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { BridgeError } from "../errors.js";
|
|
15
|
+
import { getCliCopy, } from "./cli-copy.js";
|
|
16
|
+
import { GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS, GATEWAY_CONTROL_MAX_MESSAGE_BYTES, GATEWAY_CONTROL_MAX_RESPONSE_BYTES, GATEWAY_CONTROL_PROTOCOL_VERSION, GatewayControlTransportError, isClaudeSessionSelector, isGatewayAlias, isGatewayConversationId, isGatewayDeliveryToken, isGatewayHostId, isGatewayReplyAddress, sendGatewayControlRequest, } from "./control.js";
|
|
17
|
+
import { loadGatewayConfig } from "./config.js";
|
|
18
|
+
import { isDashboardLocale, } from "./locale.js";
|
|
19
|
+
import { runLiveDashboardCommand, } from "./live-dashboard-command.js";
|
|
20
|
+
import { runGatewayServer, } from "./server.js";
|
|
21
|
+
import { PROGRESS_WATCH_DEFAULT_IDLE_MS } from "./progress-watch-machine.js";
|
|
22
|
+
import { isCompatibilityCertificationReport, isCompatibilityCheckReport, } from "./compatibility.js";
|
|
23
|
+
const THREAD_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
24
|
+
const DEFAULT_HOST_ID = "this-mac";
|
|
25
|
+
const CLI_MAX_OUTPUT_BYTES = GATEWAY_CONTROL_MAX_RESPONSE_BYTES;
|
|
26
|
+
const DELIVERY_POLL_INTERVAL_MS = 250;
|
|
27
|
+
const DELIVERY_POLL_MIN_REQUEST_TIMEOUT_MS = 50;
|
|
28
|
+
export const EMBASSY_VERSION = "1.0.0";
|
|
29
|
+
const DEFAULT_CLI_LOCALE = "en";
|
|
30
|
+
export const gatewayCliCommands = [
|
|
31
|
+
"serve",
|
|
32
|
+
"health",
|
|
33
|
+
"status",
|
|
34
|
+
"compat-check",
|
|
35
|
+
"compat-certify",
|
|
36
|
+
"delivery-status",
|
|
37
|
+
"wait-delivery",
|
|
38
|
+
"untrack",
|
|
39
|
+
"refresh-dashboard",
|
|
40
|
+
"dashboard",
|
|
41
|
+
"register-codex",
|
|
42
|
+
"unregister-codex",
|
|
43
|
+
"select-claude",
|
|
44
|
+
"unselect-claude",
|
|
45
|
+
"pair",
|
|
46
|
+
"unpair",
|
|
47
|
+
"send-to-claude",
|
|
48
|
+
"send-to-codex",
|
|
49
|
+
"reply",
|
|
50
|
+
];
|
|
51
|
+
export const gatewayCliExitCodes = Object.freeze({
|
|
52
|
+
ok: 0,
|
|
53
|
+
invalidInput: 2,
|
|
54
|
+
rejected: 3,
|
|
55
|
+
unavailable: 4,
|
|
56
|
+
ambiguous: 5,
|
|
57
|
+
failure: 6,
|
|
58
|
+
claudeCertificationFailed: 7,
|
|
59
|
+
codexCertificationFailed: 8,
|
|
60
|
+
multipleCertificationsFailed: 9,
|
|
61
|
+
});
|
|
62
|
+
const COMPAT_CERTIFY_CONTROL_TIMEOUT_MS = 90_000;
|
|
63
|
+
class CliFault extends Error {
|
|
64
|
+
code;
|
|
65
|
+
retryable;
|
|
66
|
+
hint;
|
|
67
|
+
kind;
|
|
68
|
+
constructor(code, retryable = false, hint, kind) {
|
|
69
|
+
super("The gateway client rejected the request.");
|
|
70
|
+
this.name = "CliFault";
|
|
71
|
+
this.code = code;
|
|
72
|
+
this.retryable = retryable;
|
|
73
|
+
this.hint = hint;
|
|
74
|
+
this.kind = kind;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function isCommand(value) {
|
|
78
|
+
return (value !== undefined &&
|
|
79
|
+
gatewayCliCommands.includes(value));
|
|
80
|
+
}
|
|
81
|
+
function parseOptions(args, valueNames, flagNames = []) {
|
|
82
|
+
const values = new Set(valueNames);
|
|
83
|
+
const flags = new Set(flagNames);
|
|
84
|
+
const parsed = Object.create(null);
|
|
85
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
86
|
+
const token = args[index];
|
|
87
|
+
if (token === undefined || !token.startsWith("--") || token.length <= 2) {
|
|
88
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
89
|
+
}
|
|
90
|
+
const name = token.slice(2);
|
|
91
|
+
if (Object.hasOwn(parsed, name)) {
|
|
92
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
93
|
+
}
|
|
94
|
+
if (flags.has(name)) {
|
|
95
|
+
parsed[name] = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (!values.has(name)) {
|
|
99
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
100
|
+
}
|
|
101
|
+
const value = args[index + 1];
|
|
102
|
+
if (value === undefined || value.startsWith("--") || value.length === 0) {
|
|
103
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
104
|
+
}
|
|
105
|
+
parsed[name] = value;
|
|
106
|
+
index += 1;
|
|
107
|
+
}
|
|
108
|
+
return parsed;
|
|
109
|
+
}
|
|
110
|
+
function environmentLocale(env) {
|
|
111
|
+
const value = env.EMBASSY_LOCALE;
|
|
112
|
+
if (value === undefined || value.length === 0)
|
|
113
|
+
return DEFAULT_CLI_LOCALE;
|
|
114
|
+
if (!isDashboardLocale(value))
|
|
115
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
function resolveCommonCliOptions(args, env) {
|
|
119
|
+
const stripped = [];
|
|
120
|
+
let locale;
|
|
121
|
+
let sawLocale = false;
|
|
122
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
123
|
+
const token = args[index];
|
|
124
|
+
if (token !== "--lang") {
|
|
125
|
+
if (token !== undefined)
|
|
126
|
+
stripped.push(token);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (sawLocale)
|
|
130
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
131
|
+
sawLocale = true;
|
|
132
|
+
const value = args[index + 1];
|
|
133
|
+
if (value === undefined || value.startsWith("--") || value.length === 0) {
|
|
134
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
135
|
+
}
|
|
136
|
+
if (!isDashboardLocale(value)) {
|
|
137
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
138
|
+
}
|
|
139
|
+
locale = value;
|
|
140
|
+
index += 1;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
args: stripped,
|
|
144
|
+
// Resolve the environment only when no valid explicit flag exists. This
|
|
145
|
+
// lets an exact flag intentionally override even a malformed environment.
|
|
146
|
+
locale: locale ?? environmentLocale(env),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function fallbackCliLocale(args, env) {
|
|
150
|
+
const localeFlagIndices = [];
|
|
151
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
152
|
+
if (args[index] === "--lang")
|
|
153
|
+
localeFlagIndices.push(index);
|
|
154
|
+
}
|
|
155
|
+
if (localeFlagIndices.length === 1) {
|
|
156
|
+
const value = args[(localeFlagIndices[0] ?? -1) + 1];
|
|
157
|
+
if (isDashboardLocale(value))
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
return isDashboardLocale(env.EMBASSY_LOCALE)
|
|
161
|
+
? env.EMBASSY_LOCALE
|
|
162
|
+
: DEFAULT_CLI_LOCALE;
|
|
163
|
+
}
|
|
164
|
+
function fixedStderr(locale, kind) {
|
|
165
|
+
return `[embassy] ${getCliCopy(locale)[`error.${kind}`]}\n`;
|
|
166
|
+
}
|
|
167
|
+
function requireString(options, name) {
|
|
168
|
+
const value = options[name];
|
|
169
|
+
if (typeof value !== "string")
|
|
170
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
function assertExactOptionCount(options, minimum, maximum = minimum) {
|
|
174
|
+
const count = Object.keys(options).length;
|
|
175
|
+
if (count < minimum || count > maximum) {
|
|
176
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function requireAlias(options, name) {
|
|
180
|
+
const alias = requireString(options, name);
|
|
181
|
+
if (!isGatewayAlias(alias))
|
|
182
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
183
|
+
return alias;
|
|
184
|
+
}
|
|
185
|
+
function requireCodexAlias(options, name) {
|
|
186
|
+
const alias = requireAlias(options, name);
|
|
187
|
+
if (!alias.startsWith("codex-"))
|
|
188
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
189
|
+
return alias;
|
|
190
|
+
}
|
|
191
|
+
function gatewayAliasHost(alias) {
|
|
192
|
+
return alias.slice(alias.lastIndexOf("@") + 1);
|
|
193
|
+
}
|
|
194
|
+
function requireClaudeSelector(options, name) {
|
|
195
|
+
const selector = requireString(options, name);
|
|
196
|
+
if (!isClaudeSessionSelector(selector)) {
|
|
197
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
198
|
+
}
|
|
199
|
+
return selector;
|
|
200
|
+
}
|
|
201
|
+
function trackIdleMinutes(options) {
|
|
202
|
+
const tracking = options.track === true;
|
|
203
|
+
const raw = options["idle-minutes"];
|
|
204
|
+
if (raw !== undefined && !tracking) {
|
|
205
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
206
|
+
}
|
|
207
|
+
if (!tracking)
|
|
208
|
+
return undefined;
|
|
209
|
+
if (raw === undefined)
|
|
210
|
+
return PROGRESS_WATCH_DEFAULT_IDLE_MS / 60_000;
|
|
211
|
+
if (typeof raw !== "string" ||
|
|
212
|
+
!/^[1-9][0-9]{0,3}$/.test(raw)) {
|
|
213
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
214
|
+
}
|
|
215
|
+
const minutes = Number(raw);
|
|
216
|
+
if (!Number.isSafeInteger(minutes) || minutes > 24 * 60) {
|
|
217
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
218
|
+
}
|
|
219
|
+
return minutes;
|
|
220
|
+
}
|
|
221
|
+
function requireDeliveryToken(options, name) {
|
|
222
|
+
const token = requireString(options, name);
|
|
223
|
+
if (!isGatewayDeliveryToken(token)) {
|
|
224
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
225
|
+
}
|
|
226
|
+
return token;
|
|
227
|
+
}
|
|
228
|
+
function requireCodexThreadId(env) {
|
|
229
|
+
const threadId = env.CODEX_THREAD_ID;
|
|
230
|
+
if (typeof threadId !== "string" || !THREAD_ID_PATTERN.test(threadId)) {
|
|
231
|
+
throw new CliFault("CODEX_IDENTITY_REQUIRED");
|
|
232
|
+
}
|
|
233
|
+
return threadId.toLowerCase();
|
|
234
|
+
}
|
|
235
|
+
function hasInheritedIdentity(value) {
|
|
236
|
+
return typeof value === "string" && value.length > 0;
|
|
237
|
+
}
|
|
238
|
+
function requireExclusiveCodexThreadId(env) {
|
|
239
|
+
if (hasInheritedIdentity(env.CLAUDE_CODE_MESSAGING_SOCKET)) {
|
|
240
|
+
throw new CliFault("CALLER_IDENTITY_CONFLICT");
|
|
241
|
+
}
|
|
242
|
+
return requireCodexThreadId(env);
|
|
243
|
+
}
|
|
244
|
+
function optionalCodexThreadId(env) {
|
|
245
|
+
const threadId = env.CODEX_THREAD_ID;
|
|
246
|
+
if (threadId === undefined || threadId.length === 0)
|
|
247
|
+
return undefined;
|
|
248
|
+
if (!THREAD_ID_PATTERN.test(threadId)) {
|
|
249
|
+
throw new CliFault("CODEX_IDENTITY_REQUIRED");
|
|
250
|
+
}
|
|
251
|
+
return threadId.toLowerCase();
|
|
252
|
+
}
|
|
253
|
+
function optionalClaudeReplyAddress(env) {
|
|
254
|
+
const socketPath = env.CLAUDE_CODE_MESSAGING_SOCKET;
|
|
255
|
+
if (socketPath === undefined || socketPath.length === 0)
|
|
256
|
+
return undefined;
|
|
257
|
+
// Claude Code 2.1.225 exports the raw socket path. The peer protocol's
|
|
258
|
+
// transient callback capability is the same path with an explicit UDS
|
|
259
|
+
// scheme; form it only in memory and leave generation validation to the
|
|
260
|
+
// broker's pinned Claude adapter.
|
|
261
|
+
if (socketPath.includes("\0") ||
|
|
262
|
+
!path.isAbsolute(socketPath) ||
|
|
263
|
+
path.resolve(socketPath) !== socketPath) {
|
|
264
|
+
throw new CliFault("CLAUDE_IDENTITY_INVALID");
|
|
265
|
+
}
|
|
266
|
+
const replyAddress = `uds:${socketPath}`;
|
|
267
|
+
if (!isGatewayReplyAddress(replyAddress)) {
|
|
268
|
+
throw new CliFault("CLAUDE_IDENTITY_INVALID");
|
|
269
|
+
}
|
|
270
|
+
return replyAddress;
|
|
271
|
+
}
|
|
272
|
+
function requireClaudeReplyAddress(env) {
|
|
273
|
+
const replyAddress = optionalClaudeReplyAddress(env);
|
|
274
|
+
if (replyAddress === undefined) {
|
|
275
|
+
throw new CliFault("CLAUDE_IDENTITY_REQUIRED");
|
|
276
|
+
}
|
|
277
|
+
return replyAddress;
|
|
278
|
+
}
|
|
279
|
+
function requireExclusiveClaudeReplyAddress(env) {
|
|
280
|
+
if (hasInheritedIdentity(env.CODEX_THREAD_ID)) {
|
|
281
|
+
throw new CliFault("CALLER_IDENTITY_CONFLICT");
|
|
282
|
+
}
|
|
283
|
+
return requireClaudeReplyAddress(env);
|
|
284
|
+
}
|
|
285
|
+
async function readMessageBody(stdin) {
|
|
286
|
+
const chunks = [];
|
|
287
|
+
let length = 0;
|
|
288
|
+
for await (const chunk of stdin) {
|
|
289
|
+
if (typeof chunk !== "string" && !Buffer.isBuffer(chunk)) {
|
|
290
|
+
throw new CliFault("INVALID_MESSAGE_INPUT");
|
|
291
|
+
}
|
|
292
|
+
const buffer = Buffer.isBuffer(chunk)
|
|
293
|
+
? Buffer.from(chunk)
|
|
294
|
+
: Buffer.from(chunk, "utf8");
|
|
295
|
+
length += buffer.length;
|
|
296
|
+
if (length > GATEWAY_CONTROL_MAX_MESSAGE_BYTES) {
|
|
297
|
+
throw new CliFault("MESSAGE_TOO_LARGE");
|
|
298
|
+
}
|
|
299
|
+
chunks.push(buffer);
|
|
300
|
+
}
|
|
301
|
+
let text;
|
|
302
|
+
try {
|
|
303
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, length));
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
throw new CliFault("INVALID_MESSAGE_INPUT");
|
|
307
|
+
}
|
|
308
|
+
if (text.trim().length === 0 || text.includes("\0")) {
|
|
309
|
+
throw new CliFault("MESSAGE_REQUIRED");
|
|
310
|
+
}
|
|
311
|
+
return text;
|
|
312
|
+
}
|
|
313
|
+
function emptyParams(args) {
|
|
314
|
+
if (args.length !== 0)
|
|
315
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
316
|
+
return {};
|
|
317
|
+
}
|
|
318
|
+
function parseServeInboundMode(args) {
|
|
319
|
+
const options = parseOptions(args, ["inbound"]);
|
|
320
|
+
if (Object.keys(options).length === 0)
|
|
321
|
+
return "paired";
|
|
322
|
+
assertExactOptionCount(options, 1);
|
|
323
|
+
if (options.inbound !== "open")
|
|
324
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
325
|
+
return "open";
|
|
326
|
+
}
|
|
327
|
+
function validateLiveDashboardArgs(args) {
|
|
328
|
+
if (args.length === 0) {
|
|
329
|
+
throw new CliFault("INVALID_ARGUMENTS", false, "hint.dashboardLiveRequired");
|
|
330
|
+
}
|
|
331
|
+
const options = parseOptions(args, [], ["live"]);
|
|
332
|
+
assertExactOptionCount(options, 1);
|
|
333
|
+
if (options.live !== true)
|
|
334
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
335
|
+
}
|
|
336
|
+
async function buildRequest(command, args, env, stdin) {
|
|
337
|
+
switch (command) {
|
|
338
|
+
case "serve":
|
|
339
|
+
case "dashboard":
|
|
340
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
341
|
+
case "health":
|
|
342
|
+
return {
|
|
343
|
+
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
344
|
+
method: "health",
|
|
345
|
+
params: emptyParams(args),
|
|
346
|
+
};
|
|
347
|
+
case "status":
|
|
348
|
+
return {
|
|
349
|
+
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
350
|
+
method: "list_snapshot",
|
|
351
|
+
params: emptyParams(args),
|
|
352
|
+
};
|
|
353
|
+
case "compat-check":
|
|
354
|
+
return {
|
|
355
|
+
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
356
|
+
method: "compat_check",
|
|
357
|
+
params: emptyParams(args),
|
|
358
|
+
};
|
|
359
|
+
case "compat-certify": {
|
|
360
|
+
const options = parseOptions(args, ["codex"], ["with-turn"]);
|
|
361
|
+
assertExactOptionCount(options, 0, 2);
|
|
362
|
+
const codexAlias = options.codex === undefined
|
|
363
|
+
? undefined
|
|
364
|
+
: requireCodexAlias(options, "codex");
|
|
365
|
+
return {
|
|
366
|
+
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
367
|
+
method: "compat_certify",
|
|
368
|
+
params: {
|
|
369
|
+
withTurn: options["with-turn"] === true,
|
|
370
|
+
...(codexAlias === undefined ? {} : { codexAlias }),
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
case "delivery-status":
|
|
375
|
+
case "wait-delivery": {
|
|
376
|
+
const options = parseOptions(args, ["token"]);
|
|
377
|
+
assertExactOptionCount(options, 1);
|
|
378
|
+
return {
|
|
379
|
+
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
380
|
+
method: "delivery_status",
|
|
381
|
+
params: { token: requireDeliveryToken(options, "token") },
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
case "untrack": {
|
|
385
|
+
const options = parseOptions(args, ["conversation"]);
|
|
386
|
+
assertExactOptionCount(options, 1);
|
|
387
|
+
const conversationId = requireString(options, "conversation");
|
|
388
|
+
if (!isGatewayConversationId(conversationId)) {
|
|
389
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
393
|
+
method: "untrack",
|
|
394
|
+
params: { conversationId },
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
case "refresh-dashboard":
|
|
398
|
+
return {
|
|
399
|
+
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
400
|
+
method: "refresh_dashboard",
|
|
401
|
+
params: emptyParams(args),
|
|
402
|
+
};
|
|
403
|
+
case "register-codex": {
|
|
404
|
+
const options = parseOptions(args, ["alias", "host", "succeeds"]);
|
|
405
|
+
const alias = requireCodexAlias(options, "alias");
|
|
406
|
+
const succeedsAlias = options.succeeds === undefined
|
|
407
|
+
? undefined
|
|
408
|
+
: requireCodexAlias(options, "succeeds");
|
|
409
|
+
if (succeedsAlias === undefined) {
|
|
410
|
+
assertExactOptionCount(options, 1, 2);
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
assertExactOptionCount(options, 2);
|
|
414
|
+
if (options.host !== undefined) {
|
|
415
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const host = succeedsAlias === undefined
|
|
419
|
+
? (options.host ?? DEFAULT_HOST_ID)
|
|
420
|
+
: gatewayAliasHost(alias);
|
|
421
|
+
if (typeof host !== "string" ||
|
|
422
|
+
!isGatewayHostId(host) ||
|
|
423
|
+
!alias.endsWith(`@${host}`) ||
|
|
424
|
+
(succeedsAlias !== undefined &&
|
|
425
|
+
(succeedsAlias === alias ||
|
|
426
|
+
gatewayAliasHost(succeedsAlias) !== host))) {
|
|
427
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
431
|
+
method: "register_codex",
|
|
432
|
+
params: {
|
|
433
|
+
alias,
|
|
434
|
+
threadId: requireExclusiveCodexThreadId(env),
|
|
435
|
+
hostId: host,
|
|
436
|
+
busyPolicy: "queue",
|
|
437
|
+
...(succeedsAlias === undefined ? {} : { succeedsAlias }),
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
case "unregister-codex": {
|
|
442
|
+
const options = parseOptions(args, ["alias"]);
|
|
443
|
+
assertExactOptionCount(options, 1);
|
|
444
|
+
return {
|
|
445
|
+
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
446
|
+
method: "unregister_codex",
|
|
447
|
+
params: {
|
|
448
|
+
alias: requireCodexAlias(options, "alias"),
|
|
449
|
+
threadId: requireExclusiveCodexThreadId(env),
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
case "select-claude":
|
|
454
|
+
case "unselect-claude": {
|
|
455
|
+
const options = parseOptions(args, ["alias", "session"]);
|
|
456
|
+
assertExactOptionCount(options, 1);
|
|
457
|
+
const selector = options.alias === undefined
|
|
458
|
+
? requireClaudeSelector(options, "session")
|
|
459
|
+
: requireClaudeSelector(options, "alias");
|
|
460
|
+
const codexThreadId = optionalCodexThreadId(env);
|
|
461
|
+
return {
|
|
462
|
+
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
463
|
+
method: command === "select-claude" ? "select_claude" : "unselect_claude",
|
|
464
|
+
params: {
|
|
465
|
+
alias: selector,
|
|
466
|
+
...(codexThreadId === undefined ? {} : { codexThreadId }),
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
case "pair":
|
|
471
|
+
case "unpair": {
|
|
472
|
+
const options = parseOptions(args, ["claude", "codex"]);
|
|
473
|
+
assertExactOptionCount(options, 2);
|
|
474
|
+
return {
|
|
475
|
+
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
476
|
+
method: command,
|
|
477
|
+
params: {
|
|
478
|
+
claudeAlias: requireClaudeSelector(options, "claude"),
|
|
479
|
+
codexAlias: requireCodexAlias(options, "codex"),
|
|
480
|
+
codexThreadId: requireExclusiveCodexThreadId(env),
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
case "send-to-claude": {
|
|
485
|
+
const options = parseOptions(args, ["from", "to", "idle-minutes"], ["expects-reply", "track"]);
|
|
486
|
+
assertExactOptionCount(options, 2, 5);
|
|
487
|
+
const threadId = requireExclusiveCodexThreadId(env);
|
|
488
|
+
const idleMinutes = trackIdleMinutes(options);
|
|
489
|
+
return {
|
|
490
|
+
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
491
|
+
method: "send_to_claude",
|
|
492
|
+
params: {
|
|
493
|
+
fromAlias: requireAlias(options, "from"),
|
|
494
|
+
threadId,
|
|
495
|
+
toAlias: requireClaudeSelector(options, "to"),
|
|
496
|
+
text: await readMessageBody(stdin),
|
|
497
|
+
expectsReply: options["expects-reply"] === true,
|
|
498
|
+
...(idleMinutes === undefined
|
|
499
|
+
? {}
|
|
500
|
+
: { trackIdleMinutes: idleMinutes }),
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
case "send-to-codex": {
|
|
505
|
+
const options = parseOptions(args, ["from", "to", "idle-minutes"], ["expects-reply", "track"]);
|
|
506
|
+
assertExactOptionCount(options, 2, 5);
|
|
507
|
+
const replyAddress = requireExclusiveClaudeReplyAddress(env);
|
|
508
|
+
const idleMinutes = trackIdleMinutes(options);
|
|
509
|
+
return {
|
|
510
|
+
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
511
|
+
method: "send_to_codex",
|
|
512
|
+
params: {
|
|
513
|
+
fromAlias: requireAlias(options, "from"),
|
|
514
|
+
toAlias: requireAlias(options, "to"),
|
|
515
|
+
text: await readMessageBody(stdin),
|
|
516
|
+
replyAddress,
|
|
517
|
+
expectsReply: options["expects-reply"] === true,
|
|
518
|
+
...(idleMinutes === undefined
|
|
519
|
+
? {}
|
|
520
|
+
: { trackIdleMinutes: idleMinutes }),
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
case "reply": {
|
|
525
|
+
const options = parseOptions(args, ["conversation", "alias", "idle-minutes"], ["track"]);
|
|
526
|
+
assertExactOptionCount(options, 2, 4);
|
|
527
|
+
const conversationId = requireString(options, "conversation");
|
|
528
|
+
if (!isGatewayConversationId(conversationId)) {
|
|
529
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
530
|
+
}
|
|
531
|
+
const alias = requireAlias(options, "alias");
|
|
532
|
+
const idleMinutes = trackIdleMinutes(options);
|
|
533
|
+
const threadId = env.CODEX_THREAD_ID;
|
|
534
|
+
const replyAddress = optionalClaudeReplyAddress(env);
|
|
535
|
+
const hasCodexIdentity = hasInheritedIdentity(threadId);
|
|
536
|
+
const hasClaudeIdentity = replyAddress !== undefined;
|
|
537
|
+
if (hasCodexIdentity && hasClaudeIdentity) {
|
|
538
|
+
throw new CliFault("CALLER_IDENTITY_CONFLICT");
|
|
539
|
+
}
|
|
540
|
+
if (!hasCodexIdentity && !hasClaudeIdentity) {
|
|
541
|
+
throw new CliFault("CALLER_IDENTITY_REQUIRED");
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
545
|
+
method: "reply",
|
|
546
|
+
params: {
|
|
547
|
+
conversationId,
|
|
548
|
+
text: await readMessageBody(stdin),
|
|
549
|
+
...(idleMinutes === undefined
|
|
550
|
+
? {}
|
|
551
|
+
: { trackIdleMinutes: idleMinutes }),
|
|
552
|
+
caller: hasCodexIdentity
|
|
553
|
+
? {
|
|
554
|
+
kind: "codex",
|
|
555
|
+
alias,
|
|
556
|
+
threadId: requireCodexThreadId(env),
|
|
557
|
+
}
|
|
558
|
+
: {
|
|
559
|
+
kind: "claude",
|
|
560
|
+
alias,
|
|
561
|
+
replyAddress: requireClaudeReplyAddress(env),
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
export async function validatePrivateGatewayControlSocket(stateDir, socketPath) {
|
|
569
|
+
if (process.platform === "win32") {
|
|
570
|
+
throw new CliFault("CONTROL_SOCKET_UNAVAILABLE", true);
|
|
571
|
+
}
|
|
572
|
+
let state;
|
|
573
|
+
let socket;
|
|
574
|
+
try {
|
|
575
|
+
[state, socket] = await Promise.all([lstat(stateDir), lstat(socketPath)]);
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
throw new CliFault("CONTROL_SOCKET_UNAVAILABLE", true);
|
|
579
|
+
}
|
|
580
|
+
const getuid = process.getuid;
|
|
581
|
+
const expectedUid = getuid?.call(process);
|
|
582
|
+
if (state.isSymbolicLink() ||
|
|
583
|
+
!state.isDirectory() ||
|
|
584
|
+
(state.mode & 0o700) !== 0o700 ||
|
|
585
|
+
(state.mode & 0o077) !== 0 ||
|
|
586
|
+
(expectedUid !== undefined && state.uid !== expectedUid)) {
|
|
587
|
+
throw new CliFault("CONTROL_STATE_UNSAFE", false, undefined, "unsafe");
|
|
588
|
+
}
|
|
589
|
+
if (socket.isSymbolicLink() ||
|
|
590
|
+
!socket.isSocket() ||
|
|
591
|
+
(socket.mode & 0o777) !== 0o600 ||
|
|
592
|
+
(expectedUid !== undefined && socket.uid !== expectedUid)) {
|
|
593
|
+
throw new CliFault("CONTROL_SOCKET_UNSAFE", false, undefined, "unsafe");
|
|
594
|
+
}
|
|
595
|
+
let stateReal;
|
|
596
|
+
let socketParentReal;
|
|
597
|
+
try {
|
|
598
|
+
[stateReal, socketParentReal] = await Promise.all([
|
|
599
|
+
realpath(stateDir),
|
|
600
|
+
realpath(path.dirname(socketPath)),
|
|
601
|
+
]);
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
throw new CliFault("CONTROL_SOCKET_UNSAFE", false, undefined, "unsafe");
|
|
605
|
+
}
|
|
606
|
+
if (path.dirname(socketPath) !== stateDir ||
|
|
607
|
+
stateReal !== socketParentReal) {
|
|
608
|
+
throw new CliFault("CONTROL_SOCKET_UNSAFE", false, undefined, "unsafe");
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function safeCommandLabel(command) {
|
|
612
|
+
return command ?? "unknown";
|
|
613
|
+
}
|
|
614
|
+
function serializedOutput(value) {
|
|
615
|
+
const line = `${JSON.stringify(value)}\n`;
|
|
616
|
+
if (Buffer.byteLength(line, "utf8") > CLI_MAX_OUTPUT_BYTES) {
|
|
617
|
+
throw new CliFault("OUTPUT_TOO_LARGE");
|
|
618
|
+
}
|
|
619
|
+
return line;
|
|
620
|
+
}
|
|
621
|
+
function writeFailure(stdout, stderr, locale, command, code, options) {
|
|
622
|
+
const ambiguous = options.ambiguous ?? false;
|
|
623
|
+
const retryable = options.retryable ?? false;
|
|
624
|
+
stdout.write(serializedOutput({
|
|
625
|
+
ok: false,
|
|
626
|
+
command: safeCommandLabel(command),
|
|
627
|
+
error: { code, ambiguous, retryable },
|
|
628
|
+
}));
|
|
629
|
+
stderr.write(fixedStderr(locale, options.kind));
|
|
630
|
+
}
|
|
631
|
+
function isRejectedResult(result) {
|
|
632
|
+
return (result !== null &&
|
|
633
|
+
typeof result === "object" &&
|
|
634
|
+
Object.hasOwn(result, "accepted") &&
|
|
635
|
+
result.accepted === false);
|
|
636
|
+
}
|
|
637
|
+
function responseExitCode(response) {
|
|
638
|
+
if (!response.ok)
|
|
639
|
+
return gatewayCliExitCodes.failure;
|
|
640
|
+
if (isRejectedResult(response.result))
|
|
641
|
+
return gatewayCliExitCodes.rejected;
|
|
642
|
+
return gatewayCliExitCodes.ok;
|
|
643
|
+
}
|
|
644
|
+
function waitDeliveryExitCode(response) {
|
|
645
|
+
if (!response.ok ||
|
|
646
|
+
!response.result.found ||
|
|
647
|
+
!response.result.terminal) {
|
|
648
|
+
return gatewayCliExitCodes.failure;
|
|
649
|
+
}
|
|
650
|
+
return response.result.state === "delivered"
|
|
651
|
+
? gatewayCliExitCodes.ok
|
|
652
|
+
: gatewayCliExitCodes.failure;
|
|
653
|
+
}
|
|
654
|
+
async function defaultDelay(milliseconds) {
|
|
655
|
+
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
656
|
+
}
|
|
657
|
+
async function waitForDelivery(socketPath, request, sendRequest, now, delay) {
|
|
658
|
+
let clientDeadlineMs;
|
|
659
|
+
while (true) {
|
|
660
|
+
const beforeRequest = now();
|
|
661
|
+
let requestTimeoutMs = GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS;
|
|
662
|
+
if (clientDeadlineMs !== undefined) {
|
|
663
|
+
const remainingMs = clientDeadlineMs - beforeRequest;
|
|
664
|
+
if (remainingMs <= 0)
|
|
665
|
+
return { kind: "timeout" };
|
|
666
|
+
if (remainingMs < DELIVERY_POLL_MIN_REQUEST_TIMEOUT_MS) {
|
|
667
|
+
await delay(remainingMs);
|
|
668
|
+
return { kind: "timeout" };
|
|
669
|
+
}
|
|
670
|
+
requestTimeoutMs = Math.min(GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS, Math.floor(remainingMs));
|
|
671
|
+
}
|
|
672
|
+
let response;
|
|
673
|
+
try {
|
|
674
|
+
response = await sendRequest({
|
|
675
|
+
socketPath,
|
|
676
|
+
request,
|
|
677
|
+
timeoutMs: requestTimeoutMs,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
if (clientDeadlineMs !== undefined &&
|
|
682
|
+
now() >= clientDeadlineMs &&
|
|
683
|
+
error instanceof GatewayControlTransportError &&
|
|
684
|
+
error.recoverable) {
|
|
685
|
+
return { kind: "timeout" };
|
|
686
|
+
}
|
|
687
|
+
throw error;
|
|
688
|
+
}
|
|
689
|
+
if (!response.ok) {
|
|
690
|
+
if (response.error.code === "REQUEST_TIMEOUT" &&
|
|
691
|
+
clientDeadlineMs !== undefined &&
|
|
692
|
+
now() >= clientDeadlineMs) {
|
|
693
|
+
return { kind: "timeout" };
|
|
694
|
+
}
|
|
695
|
+
return { kind: "response", response };
|
|
696
|
+
}
|
|
697
|
+
if (!response.result.found)
|
|
698
|
+
return { kind: "unknown" };
|
|
699
|
+
// A retained terminal decision is authoritative even when the command is
|
|
700
|
+
// invoked after the original delivery deadline and client grace window.
|
|
701
|
+
if (response.result.terminal) {
|
|
702
|
+
return { kind: "response", response };
|
|
703
|
+
}
|
|
704
|
+
const parsedDeadlineMs = Date.parse(response.result.deadlineAt);
|
|
705
|
+
if (!Number.isFinite(parsedDeadlineMs)) {
|
|
706
|
+
throw new Error("invalid validated delivery deadline");
|
|
707
|
+
}
|
|
708
|
+
const observedClientDeadlineMs = parsedDeadlineMs + GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS;
|
|
709
|
+
clientDeadlineMs =
|
|
710
|
+
clientDeadlineMs === undefined
|
|
711
|
+
? observedClientDeadlineMs
|
|
712
|
+
: Math.min(clientDeadlineMs, observedClientDeadlineMs);
|
|
713
|
+
const afterRequest = now();
|
|
714
|
+
const remainingMs = clientDeadlineMs - afterRequest;
|
|
715
|
+
if (remainingMs <= 0)
|
|
716
|
+
return { kind: "timeout" };
|
|
717
|
+
await delay(Math.min(DELIVERY_POLL_INTERVAL_MS, remainingMs));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
/** Run one command; foreground runners own and release their signal handlers. */
|
|
721
|
+
export async function runGatewayCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
722
|
+
const env = dependencies.env ?? process.env;
|
|
723
|
+
const stdin = dependencies.stdin ?? process.stdin;
|
|
724
|
+
const stdout = dependencies.stdout ?? process.stdout;
|
|
725
|
+
const stderr = dependencies.stderr ?? process.stderr;
|
|
726
|
+
const loadConfig = dependencies.loadConfig ?? loadGatewayConfig;
|
|
727
|
+
const sendRequest = dependencies.sendRequest ?? sendGatewayControlRequest;
|
|
728
|
+
const validateControlSocket = dependencies.validateControlSocket ?? validatePrivateGatewayControlSocket;
|
|
729
|
+
const runServer = dependencies.runServer ?? runGatewayServer;
|
|
730
|
+
const runLiveDashboard = dependencies.runLiveDashboard ?? runLiveDashboardCommand;
|
|
731
|
+
const now = dependencies.now ?? Date.now;
|
|
732
|
+
const delay = dependencies.delay ?? defaultDelay;
|
|
733
|
+
if (argv.length === 1 &&
|
|
734
|
+
(argv[0] === "--version" || argv[0] === "-v")) {
|
|
735
|
+
stdout.write(`embassy ${EMBASSY_VERSION}\n`);
|
|
736
|
+
return gatewayCliExitCodes.ok;
|
|
737
|
+
}
|
|
738
|
+
const command = isCommand(argv[0]) ? argv[0] : undefined;
|
|
739
|
+
let locale = fallbackCliLocale(argv.slice(1), env);
|
|
740
|
+
let serverReadyEmitted = false;
|
|
741
|
+
let liveDashboardReadyEmitted = false;
|
|
742
|
+
try {
|
|
743
|
+
const common = resolveCommonCliOptions(argv.slice(1), env);
|
|
744
|
+
locale = common.locale;
|
|
745
|
+
if (argv.length === 0 ||
|
|
746
|
+
argv[0] === "--help" ||
|
|
747
|
+
argv[0] === "-h") {
|
|
748
|
+
emptyParams(common.args);
|
|
749
|
+
stdout.write(getCliCopy(locale)["help.usage"]);
|
|
750
|
+
return gatewayCliExitCodes.ok;
|
|
751
|
+
}
|
|
752
|
+
if (command === undefined)
|
|
753
|
+
throw new CliFault("UNKNOWN_COMMAND");
|
|
754
|
+
if (command === "serve") {
|
|
755
|
+
const inboundMode = parseServeInboundMode(common.args);
|
|
756
|
+
await runServer({
|
|
757
|
+
env,
|
|
758
|
+
locale,
|
|
759
|
+
inboundMode,
|
|
760
|
+
...(dependencies.serverSignal === undefined
|
|
761
|
+
? {}
|
|
762
|
+
: { signal: dependencies.serverSignal }),
|
|
763
|
+
onReady: async (result) => {
|
|
764
|
+
if (serverReadyEmitted) {
|
|
765
|
+
throw new CliFault("SERVER_READY_ALREADY_EMITTED");
|
|
766
|
+
}
|
|
767
|
+
stdout.write(serializedOutput({
|
|
768
|
+
ok: true,
|
|
769
|
+
command,
|
|
770
|
+
result,
|
|
771
|
+
}));
|
|
772
|
+
serverReadyEmitted = true;
|
|
773
|
+
},
|
|
774
|
+
});
|
|
775
|
+
if (!serverReadyEmitted) {
|
|
776
|
+
throw new CliFault("SERVER_NOT_READY");
|
|
777
|
+
}
|
|
778
|
+
return gatewayCliExitCodes.ok;
|
|
779
|
+
}
|
|
780
|
+
if (command === "dashboard") {
|
|
781
|
+
validateLiveDashboardArgs(common.args);
|
|
782
|
+
const outcome = await runLiveDashboard({
|
|
783
|
+
env,
|
|
784
|
+
locale,
|
|
785
|
+
...(dependencies.liveDashboardSignal === undefined
|
|
786
|
+
? {}
|
|
787
|
+
: { signal: dependencies.liveDashboardSignal }),
|
|
788
|
+
loadConfig,
|
|
789
|
+
validateControlSocket,
|
|
790
|
+
sendRequest,
|
|
791
|
+
onReady: async (result) => {
|
|
792
|
+
if (liveDashboardReadyEmitted) {
|
|
793
|
+
throw new CliFault("LIVE_DASHBOARD_READY_ALREADY_EMITTED");
|
|
794
|
+
}
|
|
795
|
+
stdout.write(serializedOutput({
|
|
796
|
+
ok: true,
|
|
797
|
+
command,
|
|
798
|
+
result,
|
|
799
|
+
}));
|
|
800
|
+
liveDashboardReadyEmitted = true;
|
|
801
|
+
},
|
|
802
|
+
});
|
|
803
|
+
if (!liveDashboardReadyEmitted) {
|
|
804
|
+
if (outcome?.status === "cancelled") {
|
|
805
|
+
return gatewayCliExitCodes.ok;
|
|
806
|
+
}
|
|
807
|
+
throw new Error("LIVE_DASHBOARD_NOT_READY");
|
|
808
|
+
}
|
|
809
|
+
return gatewayCliExitCodes.ok;
|
|
810
|
+
}
|
|
811
|
+
const request = await buildRequest(command, common.args, env, stdin);
|
|
812
|
+
const config = loadConfig(env);
|
|
813
|
+
await validateControlSocket(config.stateDir, config.controlSocketPath);
|
|
814
|
+
let response;
|
|
815
|
+
let waitedDeliveryResponse;
|
|
816
|
+
if (command === "wait-delivery") {
|
|
817
|
+
if (request.method !== "delivery_status") {
|
|
818
|
+
throw new CliFault("INVALID_ARGUMENTS");
|
|
819
|
+
}
|
|
820
|
+
const outcome = await waitForDelivery(config.controlSocketPath, request, sendRequest, now, delay);
|
|
821
|
+
if (outcome.kind === "unknown") {
|
|
822
|
+
writeFailure(stdout, stderr, locale, command, "DELIVERY_TOKEN_UNKNOWN", {
|
|
823
|
+
kind: "tokenUnknown",
|
|
824
|
+
});
|
|
825
|
+
return gatewayCliExitCodes.rejected;
|
|
826
|
+
}
|
|
827
|
+
if (outcome.kind === "timeout") {
|
|
828
|
+
writeFailure(stdout, stderr, locale, command, "DELIVERY_WAIT_TIMEOUT", {
|
|
829
|
+
retryable: true,
|
|
830
|
+
kind: "deliveryTimeout",
|
|
831
|
+
});
|
|
832
|
+
return gatewayCliExitCodes.unavailable;
|
|
833
|
+
}
|
|
834
|
+
waitedDeliveryResponse = outcome.response;
|
|
835
|
+
response = waitedDeliveryResponse;
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
response = await sendRequest({
|
|
839
|
+
socketPath: config.controlSocketPath,
|
|
840
|
+
request,
|
|
841
|
+
...(command === "compat-certify"
|
|
842
|
+
? { timeoutMs: COMPAT_CERTIFY_CONTROL_TIMEOUT_MS }
|
|
843
|
+
: {}),
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
if (!response.ok) {
|
|
847
|
+
writeFailure(stdout, stderr, locale, command, response.error.code, {
|
|
848
|
+
kind: "failure",
|
|
849
|
+
});
|
|
850
|
+
return gatewayCliExitCodes.failure;
|
|
851
|
+
}
|
|
852
|
+
stdout.write(serializedOutput({
|
|
853
|
+
ok: true,
|
|
854
|
+
command,
|
|
855
|
+
result: response.result,
|
|
856
|
+
}));
|
|
857
|
+
if (command === "compat-check" && isCompatibilityCheckReport(response.result)) {
|
|
858
|
+
for (const surface of response.result.surfaces) {
|
|
859
|
+
stderr.write(`[embassy] ${surface.surface}: ${surface.tier} (${surface.version})${surface.safeErrorCode === undefined
|
|
860
|
+
? ""
|
|
861
|
+
: ` — ${surface.safeErrorCode}`}\n`);
|
|
862
|
+
}
|
|
863
|
+
return response.result.compatible
|
|
864
|
+
? gatewayCliExitCodes.ok
|
|
865
|
+
: gatewayCliExitCodes.failure;
|
|
866
|
+
}
|
|
867
|
+
if (command === "compat-certify" &&
|
|
868
|
+
isCompatibilityCertificationReport(response.result)) {
|
|
869
|
+
for (const surface of response.result.surfaces) {
|
|
870
|
+
const certification = surface.certification;
|
|
871
|
+
if (certification === undefined)
|
|
872
|
+
continue;
|
|
873
|
+
stderr.write(`[embassy] ${surface.surface}: ${certification.outcome} (${certification.depth})${certification.safeErrorCode === undefined
|
|
874
|
+
? ""
|
|
875
|
+
: ` — ${certification.safeErrorCode}`}\n`);
|
|
876
|
+
}
|
|
877
|
+
if (response.result.certified)
|
|
878
|
+
return gatewayCliExitCodes.ok;
|
|
879
|
+
const failedSurfaces = response.result.surfaces.filter((surface) => surface.certification?.outcome === "fail");
|
|
880
|
+
if (failedSurfaces.length > 1) {
|
|
881
|
+
return gatewayCliExitCodes.multipleCertificationsFailed;
|
|
882
|
+
}
|
|
883
|
+
return failedSurfaces[0]?.surface === "claude"
|
|
884
|
+
? gatewayCliExitCodes.claudeCertificationFailed
|
|
885
|
+
: gatewayCliExitCodes.codexCertificationFailed;
|
|
886
|
+
}
|
|
887
|
+
const exitCode = waitedDeliveryResponse === undefined
|
|
888
|
+
? responseExitCode(response)
|
|
889
|
+
: waitDeliveryExitCode(waitedDeliveryResponse);
|
|
890
|
+
if (exitCode === gatewayCliExitCodes.rejected) {
|
|
891
|
+
stderr.write(fixedStderr(locale, "decision"));
|
|
892
|
+
}
|
|
893
|
+
else if (command === "wait-delivery" &&
|
|
894
|
+
exitCode === gatewayCliExitCodes.failure) {
|
|
895
|
+
stderr.write(fixedStderr(locale, "failure"));
|
|
896
|
+
}
|
|
897
|
+
return exitCode;
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
if ((command === "serve" && serverReadyEmitted) ||
|
|
901
|
+
(command === "dashboard" && liveDashboardReadyEmitted)) {
|
|
902
|
+
stderr.write(fixedStderr(locale, "failure"));
|
|
903
|
+
return gatewayCliExitCodes.failure;
|
|
904
|
+
}
|
|
905
|
+
if (error instanceof GatewayControlTransportError) {
|
|
906
|
+
const ambiguous = error.ambiguous;
|
|
907
|
+
writeFailure(stdout, stderr, locale, command, error.code, {
|
|
908
|
+
ambiguous,
|
|
909
|
+
retryable: ambiguous ? false : error.recoverable,
|
|
910
|
+
kind: ambiguous ? "ambiguous" : "unavailable",
|
|
911
|
+
});
|
|
912
|
+
return ambiguous
|
|
913
|
+
? gatewayCliExitCodes.ambiguous
|
|
914
|
+
: gatewayCliExitCodes.unavailable;
|
|
915
|
+
}
|
|
916
|
+
if (error instanceof CliFault) {
|
|
917
|
+
writeFailure(stdout, stderr, locale, command, error.code, {
|
|
918
|
+
retryable: error.retryable,
|
|
919
|
+
kind: error.kind ?? (error.retryable ? "unavailable" : "input"),
|
|
920
|
+
});
|
|
921
|
+
if (error.hint !== undefined) {
|
|
922
|
+
stderr.write(`[embassy] ${getCliCopy(locale)[error.hint]}\n`);
|
|
923
|
+
}
|
|
924
|
+
return error.retryable
|
|
925
|
+
? gatewayCliExitCodes.unavailable
|
|
926
|
+
: gatewayCliExitCodes.invalidInput;
|
|
927
|
+
}
|
|
928
|
+
if (error instanceof BridgeError) {
|
|
929
|
+
writeFailure(stdout, stderr, locale, command, error.code, {
|
|
930
|
+
retryable: error.recoverable,
|
|
931
|
+
kind: error.code === "CLAUDE_VERSION_DRIFT"
|
|
932
|
+
? "versionDrift"
|
|
933
|
+
: error.recoverable
|
|
934
|
+
? "unavailable"
|
|
935
|
+
: "input",
|
|
936
|
+
});
|
|
937
|
+
return error.recoverable
|
|
938
|
+
? gatewayCliExitCodes.unavailable
|
|
939
|
+
: gatewayCliExitCodes.invalidInput;
|
|
940
|
+
}
|
|
941
|
+
writeFailure(stdout, stderr, locale, command, "INTERNAL_ERROR", {
|
|
942
|
+
kind: "failure",
|
|
943
|
+
});
|
|
944
|
+
return gatewayCliExitCodes.failure;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
function isDirectExecution() {
|
|
948
|
+
const entry = process.argv[1];
|
|
949
|
+
if (entry === undefined)
|
|
950
|
+
return false;
|
|
951
|
+
try {
|
|
952
|
+
// npm exposes package binaries through symlinks. Compare canonical paths
|
|
953
|
+
// so an installed `embassy` executable is recognized as the entry point,
|
|
954
|
+
// while importing this module in tests remains side-effect free.
|
|
955
|
+
return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
|
|
956
|
+
}
|
|
957
|
+
catch {
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (isDirectExecution()) {
|
|
962
|
+
void runGatewayCli().then((exitCode) => {
|
|
963
|
+
process.exitCode = exitCode;
|
|
964
|
+
}, () => {
|
|
965
|
+
process.stderr.write(fixedStderr(fallbackCliLocale(process.argv.slice(3), process.env), "failure"));
|
|
966
|
+
process.exitCode = gatewayCliExitCodes.failure;
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
//# sourceMappingURL=cli.js.map
|