agent-embassy 3.0.0 → 3.1.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 +21 -0
- package/CONTRIBUTING.md +4 -2
- package/README.md +26 -7
- package/SECURITY.md +17 -4
- package/dist/src/gateway/claude-helper-supervisor.js +36 -9
- package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
- package/dist/src/gateway/claude-helper.js.map +1 -1
- package/dist/src/gateway/claude-peer.js +1 -1
- package/dist/src/gateway/claude-peer.js.map +1 -1
- package/dist/src/gateway/cli.d.ts +2 -3
- package/dist/src/gateway/cli.js +135 -106
- package/dist/src/gateway/cli.js.map +1 -1
- package/dist/src/gateway/config.js +1 -1
- package/dist/src/gateway/config.js.map +1 -1
- package/dist/src/gateway/control.d.ts +23 -5
- package/dist/src/gateway/control.js +41 -11
- package/dist/src/gateway/control.js.map +1 -1
- package/dist/src/gateway/peer-protocol.d.ts +5 -0
- package/dist/src/gateway/peer-protocol.js +9 -0
- package/dist/src/gateway/peer-protocol.js.map +1 -1
- package/dist/src/gateway/providers.d.ts +4 -3
- package/dist/src/gateway/providers.js +33 -30
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/server.d.ts +1 -2
- package/dist/src/gateway/server.js +1 -4
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +6 -8
- package/dist/src/gateway/service.js +313 -252
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/status-view.d.ts +2 -50
- package/dist/src/gateway/status-view.js +66 -70
- package/dist/src/gateway/status-view.js.map +1 -1
- package/dist/src/gateway/store.d.ts +4 -1
- package/dist/src/gateway/store.js +12 -11
- package/dist/src/gateway/store.js.map +1 -1
- package/dist/src/gateway/types.d.ts +17 -4
- package/dist/src/gateway/types.js +27 -36
- package/dist/src/gateway/types.js.map +1 -1
- package/docs/CONFIGURATION.md +3 -3
- package/docs/GATEWAY-ARCHITECTURE.md +27 -21
- package/package.json +1 -1
- package/skills/embassy-peer/SKILL.md +7 -5
- package/dist/src/gateway/claude-helper-client.d.ts +0 -2
- package/dist/src/gateway/claude-helper-client.js +0 -2
- package/dist/src/gateway/claude-helper-client.js.map +0 -1
package/dist/src/gateway/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ const CLI_MAX_OUTPUT_BYTES = GATEWAY_CONTROL_MAX_RESPONSE_BYTES;
|
|
|
20
20
|
const DELIVERY_POLL_INTERVAL_MS = 250;
|
|
21
21
|
const DELIVERY_POLL_MIN_REQUEST_TIMEOUT_MS = 50;
|
|
22
22
|
const PEER_AWAIT_REQUEST_TIMEOUT_MS = 35_000;
|
|
23
|
-
export const EMBASSY_VERSION = "3.
|
|
23
|
+
export const EMBASSY_VERSION = "3.1.0";
|
|
24
24
|
// RELEASE VERSION SWEEP — every place the version lives, seven sites:
|
|
25
25
|
// package.json; npm-shrinkwrap.json (two: the root and packages[""]); this
|
|
26
26
|
// constant; test/gateway-cli.test.ts's literal package-version assertion; and
|
|
@@ -33,7 +33,7 @@ export const gatewayCliCommands = [
|
|
|
33
33
|
"unregister-codex",
|
|
34
34
|
"send", "reply",
|
|
35
35
|
"register-peer", "unregister-peer", "await",
|
|
36
|
-
"peer-stdio",
|
|
36
|
+
"peer-stdio", "retire",
|
|
37
37
|
];
|
|
38
38
|
export const gatewayCliExitCodes = Object.freeze({
|
|
39
39
|
ok: 0,
|
|
@@ -60,8 +60,11 @@ Commands:
|
|
|
60
60
|
check [--to <alias>] [--timeout <s>]
|
|
61
61
|
Round-trip self-test against a registered peer
|
|
62
62
|
refresh Rescan for Claude sessions
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
retire --alias <alias> Remove a local route and report settled work
|
|
64
|
+
register-codex --alias <codex-alias> [--succeeds <old-alias>]
|
|
65
|
+
Register or succeed a Codex task
|
|
66
|
+
unregister-codex --alias <codex-alias>
|
|
67
|
+
Unregister the current Codex task
|
|
65
68
|
register-peer --alias <peer-alias> [--token-stdin|--emit-env]
|
|
66
69
|
Register a universal shell peer
|
|
67
70
|
unregister-peer --alias <peer-alias> [--token-stdin]
|
|
@@ -69,12 +72,15 @@ Commands:
|
|
|
69
72
|
await --alias <peer-alias> [--token-stdin]
|
|
70
73
|
Wait for one peer message and acknowledge stdout
|
|
71
74
|
peer-stdio Serve the bounded federation protocol on stdin/stdout
|
|
72
|
-
send
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
send --from <own-alias> --to <alias-or-uuid> [--expects-reply]
|
|
76
|
+
send --from <own-alias> --conversation <token>
|
|
77
|
+
Exactly one of --to and --conversation; body from stdin.
|
|
78
|
+
A discovered Claude session's route installs on first send
|
|
75
79
|
reply Deprecated alias for send --conversation
|
|
76
|
-
delivery-status
|
|
77
|
-
|
|
80
|
+
delivery-status --token <delivery-token>
|
|
81
|
+
Read a delivery token
|
|
82
|
+
wait-delivery --token <delivery-token>
|
|
83
|
+
Wait for terminal delivery status
|
|
78
84
|
|
|
79
85
|
Options:
|
|
80
86
|
--token-stdin Read the peer token as the first LF-terminated stdin line
|
|
@@ -120,7 +126,8 @@ const CLI_HINT = {
|
|
|
120
126
|
stateSyncFailed: "nodes.json was written at {stateDir} but the directory could not be synced; start again and check the volume",
|
|
121
127
|
unknownTarget: "no current route answers to that name. A Claude session is addressed by its live name: run embassy refresh, then read embassy status for the name it has now.",
|
|
122
128
|
aliasCollision: "the alias names more than one live session; rename one, or address the session by UUID with --to <session-uuid>.",
|
|
123
|
-
|
|
129
|
+
workspaceBroad: "that session's workspace is a filesystem root or overlaps a temporary root; reopen the session in a specific project directory outside temporary roots.",
|
|
130
|
+
workspaceUnsafe: "the session workspace or user home failed directory checks; verify access, ownership, permissions and canonical, non-symlink paths before retrying.",
|
|
124
131
|
callerAliasMismatch: "--from must be the sending session's own alias; read the name embassy status shows for this session.",
|
|
125
132
|
targetChanged: "the session you addressed renamed or exited while the send was being set up; run embassy refresh and address it by its current name.",
|
|
126
133
|
checkNoTarget: "no Codex task is registered, so there is nothing to check. Run `embassy register-codex --alias codex-<name>@{localHost}` from inside the task, or name any current route with `embassy check --to <alias>`.",
|
|
@@ -246,7 +253,7 @@ function requireConversationId(options, name) {
|
|
|
246
253
|
fault();
|
|
247
254
|
return conversationId;
|
|
248
255
|
}
|
|
249
|
-
function
|
|
256
|
+
function requireDestinationSelector(options, name) {
|
|
250
257
|
const selector = requireString(options, name);
|
|
251
258
|
if (!isClaudeSessionSelector(selector))
|
|
252
259
|
fault();
|
|
@@ -422,6 +429,14 @@ async function buildRequest(command, args, env, stdin, loadLocalHost) {
|
|
|
422
429
|
throw aliasHostFault(local, gatewayAliasHost(alias));
|
|
423
430
|
return envelope("unregister_codex", { alias, threadId });
|
|
424
431
|
}
|
|
432
|
+
case "retire": {
|
|
433
|
+
const options = parseOptions(args, ["alias"]);
|
|
434
|
+
count(options, 1, 1);
|
|
435
|
+
const alias = requireAlias(options, "alias"), local = await loadLocalHost();
|
|
436
|
+
if (gatewayAliasHost(alias) !== local.host)
|
|
437
|
+
throw aliasHostFault(local, gatewayAliasHost(alias));
|
|
438
|
+
return envelope("retire_route", { alias });
|
|
439
|
+
}
|
|
425
440
|
case "register-peer":
|
|
426
441
|
case "unregister-peer":
|
|
427
442
|
case "await": {
|
|
@@ -463,7 +478,7 @@ async function buildRequest(command, args, env, stdin, loadLocalHost) {
|
|
|
463
478
|
const conversationId = options.conversation === undefined
|
|
464
479
|
? undefined : requireConversationId(options, "conversation");
|
|
465
480
|
const toAlias = options.to === undefined
|
|
466
|
-
? undefined :
|
|
481
|
+
? undefined : requireDestinationSelector(options, "to");
|
|
467
482
|
// One target, and a conversation is always answered expecting a reply.
|
|
468
483
|
if ((toAlias === undefined) === (conversationId === undefined))
|
|
469
484
|
fault();
|
|
@@ -618,8 +633,10 @@ function refusalHint(request, result) {
|
|
|
618
633
|
const from = request.method === "send" ? request.params.fromAlias : undefined;
|
|
619
634
|
if (reason === "PEER_ALIAS_COLLISION")
|
|
620
635
|
return { hint: "aliasCollision" };
|
|
621
|
-
if (
|
|
622
|
-
return { hint: "
|
|
636
|
+
if (reason === "CLAUDE_PEER_WORKSPACE_BROAD")
|
|
637
|
+
return { hint: "workspaceBroad" };
|
|
638
|
+
if (reason === "CLAUDE_PEER_WORKSPACE_UNSAFE")
|
|
639
|
+
return { hint: "workspaceUnsafe" };
|
|
623
640
|
// A Codex task with no registration under its alias — every task, after a
|
|
624
641
|
// private state reset — is told to register; a registration held by another
|
|
625
642
|
// task is a genuine mismatch with its own remedy; a conversation answered
|
|
@@ -645,6 +662,11 @@ function writeUnknownMethodHint(stderr, code) {
|
|
|
645
662
|
if (code === "UNKNOWN_METHOD")
|
|
646
663
|
stderr.write(`[embassy] ${CLI_HINT.unknownMethod}\n`);
|
|
647
664
|
}
|
|
665
|
+
function reportControlFailure(command, code, stdout, stderr) {
|
|
666
|
+
writeFailure(stdout, stderr, command, code, { kind: "failure" });
|
|
667
|
+
writeUnknownMethodHint(stderr, code);
|
|
668
|
+
return gatewayCliExitCodes.failure;
|
|
669
|
+
}
|
|
648
670
|
function responseExitCode(response) {
|
|
649
671
|
return !response.ok ? gatewayCliExitCodes.failure
|
|
650
672
|
: isRejectedResult(response.result) ? gatewayCliExitCodes.rejected : gatewayCliExitCodes.ok;
|
|
@@ -1042,6 +1064,92 @@ async function runGatewayCheck(options) {
|
|
|
1042
1064
|
return summarize(exitCode);
|
|
1043
1065
|
}
|
|
1044
1066
|
/** Run one command; foreground runners own and release their signal handlers. */
|
|
1067
|
+
async function runServiceCommand(args, dependencies, loadIdentity) {
|
|
1068
|
+
const command = "service";
|
|
1069
|
+
const env = dependencies.env ?? process.env;
|
|
1070
|
+
const stdout = dependencies.stdout ?? process.stdout, stderr = dependencies.stderr ?? process.stderr;
|
|
1071
|
+
const sendRequest = dependencies.sendRequest ?? sendGatewayControlRequest;
|
|
1072
|
+
const validateSocket = dependencies.validateControlSocket ?? validatePrivateGatewayControlSocket;
|
|
1073
|
+
const success = (result) => {
|
|
1074
|
+
stdout.write(serializedOutput({ ok: true, command, result }));
|
|
1075
|
+
};
|
|
1076
|
+
const subcommand = args[0];
|
|
1077
|
+
if (args.length !== 1)
|
|
1078
|
+
fault();
|
|
1079
|
+
if (subcommand !== "install" && subcommand !== "uninstall" && subcommand !== "status")
|
|
1080
|
+
fault();
|
|
1081
|
+
// Identity and the state directory are validated before the first
|
|
1082
|
+
// launchd side effect. Loading them afterwards meant a missing
|
|
1083
|
+
// inventory or an unusable state root exited 2 "request rejected"
|
|
1084
|
+
// while the agent was already bootstrapped and looping.
|
|
1085
|
+
const config = subcommand === "install" ? (await loadIdentity()).config : undefined;
|
|
1086
|
+
const serviceDeps = {
|
|
1087
|
+
homeDir: (dependencies.serviceHomeDir ?? (() => userInfo().homedir))(),
|
|
1088
|
+
runLaunchctl: dependencies.runLaunchctl ?? defaultRunLaunchctl,
|
|
1089
|
+
env, execPath: process.execPath, cliPath: fileURLToPath(import.meta.url),
|
|
1090
|
+
uid: process.getuid(),
|
|
1091
|
+
delay: dependencies.delay ?? defaultDelay, now: dependencies.now ?? monotonicNow,
|
|
1092
|
+
probeHostLease: dependencies.probeHostLease ?? defaultProbeHostLease,
|
|
1093
|
+
};
|
|
1094
|
+
try {
|
|
1095
|
+
if (subcommand === "install") {
|
|
1096
|
+
const installed = await installServiceAgent(serviceDeps);
|
|
1097
|
+
const health = await pollServiceHealth(config, sendRequest, validateSocket, dependencies.delay ?? defaultDelay, dependencies.now ?? monotonicNow);
|
|
1098
|
+
if (!health.ok) {
|
|
1099
|
+
// The agent stays installed either way: this is a report about
|
|
1100
|
+
// the broker, not an install failure, so nothing is rolled back.
|
|
1101
|
+
const elapsed = (health.elapsedMs / 1000).toFixed(1);
|
|
1102
|
+
const decisive = SERVICE_HEALTH_DECISIVE.get(health.lastObserved);
|
|
1103
|
+
writeFailure(stdout, stderr, command, "SERVICE_HEALTH_UNAVAILABLE", {
|
|
1104
|
+
retryable: decisive?.retryable ?? true, kind: decisive?.kind ?? "unavailable",
|
|
1105
|
+
detail: { lastObserved: health.lastObserved },
|
|
1106
|
+
});
|
|
1107
|
+
stderr.write(decisive === undefined
|
|
1108
|
+
? `[embassy] Installed, but the broker did not answer within ${elapsed} s; last observed ${health.lastObserved}. Run \`embassy service status\` or \`embassy health\`; log: ${installed.logPath}.\n`
|
|
1109
|
+
: `[embassy] Installed, but the broker answered ${health.lastObserved} after ${elapsed} s. Run \`embassy health\` to diagnose it; log: ${installed.logPath}.\n`);
|
|
1110
|
+
if (decisive?.hint !== undefined)
|
|
1111
|
+
stderr.write(hintLine(decisive.hint, env));
|
|
1112
|
+
return decisive?.exitCode ?? gatewayCliExitCodes.unavailable;
|
|
1113
|
+
}
|
|
1114
|
+
success({ subcommand, ...installed, health });
|
|
1115
|
+
return gatewayCliExitCodes.ok;
|
|
1116
|
+
}
|
|
1117
|
+
if (subcommand === "uninstall") {
|
|
1118
|
+
success({ subcommand, ...(await uninstallServiceAgent(serviceDeps)) });
|
|
1119
|
+
return gatewayCliExitCodes.ok;
|
|
1120
|
+
}
|
|
1121
|
+
const status = await serviceAgentStatus(serviceDeps);
|
|
1122
|
+
success({ subcommand, ...status });
|
|
1123
|
+
if (status.state !== "unknown")
|
|
1124
|
+
return gatewayCliExitCodes.ok;
|
|
1125
|
+
stderr.write(fixedStderr("unavailable"));
|
|
1126
|
+
stderr.write(`[embassy] ${status.note}${status.launchctlStderr === undefined ? "" : ` launchctl: ${status.launchctlStderr}`}\n`);
|
|
1127
|
+
return gatewayCliExitCodes.unavailable;
|
|
1128
|
+
}
|
|
1129
|
+
catch (error) {
|
|
1130
|
+
// launchctl's own stderr and the instance lease's own message are the
|
|
1131
|
+
// whole value of these failures; the generic handler below discards
|
|
1132
|
+
// the message and reports only the code. A genuine filesystem failure
|
|
1133
|
+
// on this path (an unreadable plist, an undeletable one) is a real,
|
|
1134
|
+
// recoverable service failure, not an INTERNAL_ERROR — but only an
|
|
1135
|
+
// errno-shaped one. A string `code` alone would also match CliFault
|
|
1136
|
+
// and the lease's own spawn failures, relabelling faults that already
|
|
1137
|
+
// carry a truer code of their own.
|
|
1138
|
+
const errno = error !== null && typeof error === "object" &&
|
|
1139
|
+
(typeof error.errno === "number" ||
|
|
1140
|
+
typeof error.syscall === "string");
|
|
1141
|
+
if (!(error instanceof BridgeError) && !(errno && error instanceof Error))
|
|
1142
|
+
throw error;
|
|
1143
|
+
const failure = error instanceof BridgeError ? error : new BridgeError("SERVICE_AGENT_FILESYSTEM_FAILED", `The service command could not complete: ${boundedServiceDetail(error.message)}`, true);
|
|
1144
|
+
writeFailure(stdout, stderr, command, failure.code, {
|
|
1145
|
+
retryable: failure.recoverable,
|
|
1146
|
+
kind: failure.code === "SERVICE_AGENT_PATH_UNSAFE" ? "unsafe"
|
|
1147
|
+
: failure.recoverable ? "unavailable" : "input",
|
|
1148
|
+
});
|
|
1149
|
+
stderr.write(`[embassy] ${failure.message}\n`);
|
|
1150
|
+
return failure.recoverable ? gatewayCliExitCodes.unavailable : gatewayCliExitCodes.invalidInput;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1045
1153
|
export async function runGatewayCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
1046
1154
|
const env = dependencies.env ?? process.env;
|
|
1047
1155
|
const stdin = dependencies.stdin ?? process.stdin, stdout = dependencies.stdout ?? process.stdout;
|
|
@@ -1100,7 +1208,12 @@ export async function runGatewayCli(argv = process.argv.slice(2), dependencies =
|
|
|
1100
1208
|
},
|
|
1101
1209
|
handoff: async (handoff) => {
|
|
1102
1210
|
firstCatalog = undefined;
|
|
1103
|
-
|
|
1211
|
+
const result = await request("peer_handoff", { peerHost: peerHost, handoff });
|
|
1212
|
+
if (!result.accepted)
|
|
1213
|
+
throw new PeerHandlerError({
|
|
1214
|
+
code: -32000, message: "Peer handoff refused", data: result
|
|
1215
|
+
});
|
|
1216
|
+
return result;
|
|
1104
1217
|
},
|
|
1105
1218
|
},
|
|
1106
1219
|
});
|
|
@@ -1112,84 +1225,8 @@ export async function runGatewayCli(argv = process.argv.slice(2), dependencies =
|
|
|
1112
1225
|
return gatewayCliExitCodes.unavailable;
|
|
1113
1226
|
}
|
|
1114
1227
|
}
|
|
1115
|
-
if (command === "service")
|
|
1116
|
-
|
|
1117
|
-
if (args.length !== 1)
|
|
1118
|
-
fault();
|
|
1119
|
-
if (subcommand !== "install" && subcommand !== "uninstall" && subcommand !== "status")
|
|
1120
|
-
fault();
|
|
1121
|
-
// Identity and the state directory are validated before the first
|
|
1122
|
-
// launchd side effect. Loading them afterwards meant a missing
|
|
1123
|
-
// inventory or an unusable state root exited 2 "request rejected"
|
|
1124
|
-
// while the agent was already bootstrapped and looping.
|
|
1125
|
-
const config = subcommand === "install" ? (await loadIdentity()).config : undefined;
|
|
1126
|
-
const serviceDeps = {
|
|
1127
|
-
homeDir: (dependencies.serviceHomeDir ?? (() => userInfo().homedir))(),
|
|
1128
|
-
runLaunchctl: dependencies.runLaunchctl ?? defaultRunLaunchctl,
|
|
1129
|
-
env, execPath: process.execPath, cliPath: fileURLToPath(import.meta.url),
|
|
1130
|
-
uid: process.getuid(),
|
|
1131
|
-
delay: dependencies.delay ?? defaultDelay, now: dependencies.now ?? monotonicNow,
|
|
1132
|
-
probeHostLease: dependencies.probeHostLease ?? defaultProbeHostLease,
|
|
1133
|
-
};
|
|
1134
|
-
try {
|
|
1135
|
-
if (subcommand === "install") {
|
|
1136
|
-
const installed = await installServiceAgent(serviceDeps);
|
|
1137
|
-
const health = await pollServiceHealth(config, sendRequest, validateSocket, dependencies.delay ?? defaultDelay, dependencies.now ?? monotonicNow);
|
|
1138
|
-
if (!health.ok) {
|
|
1139
|
-
// The agent stays installed either way: this is a report about
|
|
1140
|
-
// the broker, not an install failure, so nothing is rolled back.
|
|
1141
|
-
const elapsed = (health.elapsedMs / 1000).toFixed(1);
|
|
1142
|
-
const decisive = SERVICE_HEALTH_DECISIVE.get(health.lastObserved);
|
|
1143
|
-
writeFailure(stdout, stderr, command, "SERVICE_HEALTH_UNAVAILABLE", {
|
|
1144
|
-
retryable: decisive?.retryable ?? true, kind: decisive?.kind ?? "unavailable",
|
|
1145
|
-
detail: { lastObserved: health.lastObserved },
|
|
1146
|
-
});
|
|
1147
|
-
stderr.write(decisive === undefined
|
|
1148
|
-
? `[embassy] Installed, but the broker did not answer within ${elapsed} s; last observed ${health.lastObserved}. Run \`embassy service status\` or \`embassy health\`; log: ${installed.logPath}.\n`
|
|
1149
|
-
: `[embassy] Installed, but the broker answered ${health.lastObserved} after ${elapsed} s. Run \`embassy health\` to diagnose it; log: ${installed.logPath}.\n`);
|
|
1150
|
-
if (decisive?.hint !== undefined)
|
|
1151
|
-
stderr.write(hintLine(decisive.hint, env));
|
|
1152
|
-
return decisive?.exitCode ?? gatewayCliExitCodes.unavailable;
|
|
1153
|
-
}
|
|
1154
|
-
success({ subcommand, ...installed, health });
|
|
1155
|
-
return gatewayCliExitCodes.ok;
|
|
1156
|
-
}
|
|
1157
|
-
if (subcommand === "uninstall") {
|
|
1158
|
-
success({ subcommand, ...(await uninstallServiceAgent(serviceDeps)) });
|
|
1159
|
-
return gatewayCliExitCodes.ok;
|
|
1160
|
-
}
|
|
1161
|
-
const status = await serviceAgentStatus(serviceDeps);
|
|
1162
|
-
success({ subcommand, ...status });
|
|
1163
|
-
if (status.state !== "unknown")
|
|
1164
|
-
return gatewayCliExitCodes.ok;
|
|
1165
|
-
stderr.write(fixedStderr("unavailable"));
|
|
1166
|
-
stderr.write(`[embassy] ${status.note}${status.launchctlStderr === undefined ? "" : ` launchctl: ${status.launchctlStderr}`}\n`);
|
|
1167
|
-
return gatewayCliExitCodes.unavailable;
|
|
1168
|
-
}
|
|
1169
|
-
catch (error) {
|
|
1170
|
-
// launchctl's own stderr and the instance lease's own message are the
|
|
1171
|
-
// whole value of these failures; the generic handler below discards
|
|
1172
|
-
// the message and reports only the code. A genuine filesystem failure
|
|
1173
|
-
// on this path (an unreadable plist, an undeletable one) is a real,
|
|
1174
|
-
// recoverable service failure, not an INTERNAL_ERROR — but only an
|
|
1175
|
-
// errno-shaped one. A string `code` alone would also match CliFault
|
|
1176
|
-
// and the lease's own spawn failures, relabelling faults that already
|
|
1177
|
-
// carry a truer code of their own.
|
|
1178
|
-
const errno = error !== null && typeof error === "object" &&
|
|
1179
|
-
(typeof error.errno === "number" ||
|
|
1180
|
-
typeof error.syscall === "string");
|
|
1181
|
-
if (!(error instanceof BridgeError) && !(errno && error instanceof Error))
|
|
1182
|
-
throw error;
|
|
1183
|
-
const failure = error instanceof BridgeError ? error : new BridgeError("SERVICE_AGENT_FILESYSTEM_FAILED", `The service command could not complete: ${boundedServiceDetail(error.message)}`, true);
|
|
1184
|
-
writeFailure(stdout, stderr, command, failure.code, {
|
|
1185
|
-
retryable: failure.recoverable,
|
|
1186
|
-
kind: failure.code === "SERVICE_AGENT_PATH_UNSAFE" ? "unsafe"
|
|
1187
|
-
: failure.recoverable ? "unavailable" : "input",
|
|
1188
|
-
});
|
|
1189
|
-
stderr.write(`[embassy] ${failure.message}\n`);
|
|
1190
|
-
return failure.recoverable ? gatewayCliExitCodes.unavailable : gatewayCliExitCodes.invalidInput;
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1228
|
+
if (command === "service")
|
|
1229
|
+
return await runServiceCommand(args, dependencies, loadIdentity);
|
|
1193
1230
|
if (command === "status" || command === "watch" || command === "check") {
|
|
1194
1231
|
// Options are parsed before anything is loaded or contacted, so an
|
|
1195
1232
|
// unknown flag is a flat argument fault with no side effect at all.
|
|
@@ -1199,7 +1236,7 @@ export async function runGatewayCli(argv = process.argv.slice(2), dependencies =
|
|
|
1199
1236
|
count(options, 0, 2);
|
|
1200
1237
|
const recent = boundedOption(options, "recent", STATUS_RECENT.default, STATUS_RECENT.minimum, STATUS_RECENT.maximum);
|
|
1201
1238
|
const timeoutSeconds = boundedOption(options, "timeout", CHECK_TIMEOUT_DEFAULT_SECONDS, 1, CHECK_TIMEOUT_MAX_SECONDS);
|
|
1202
|
-
const target = options.to === undefined ? undefined :
|
|
1239
|
+
const target = options.to === undefined ? undefined : requireDestinationSelector(options, "to");
|
|
1203
1240
|
const { config } = await loadIdentity();
|
|
1204
1241
|
await validateSocket(config.stateDir, config.controlSocketPath);
|
|
1205
1242
|
const ask = async (method, params) => await sendRequest({
|
|
@@ -1215,9 +1252,7 @@ export async function runGatewayCli(argv = process.argv.slice(2), dependencies =
|
|
|
1215
1252
|
// The header reports how old the scan is and offers `embassy refresh`.
|
|
1216
1253
|
const response = await ask("list_snapshot", {});
|
|
1217
1254
|
if (!response.ok) {
|
|
1218
|
-
|
|
1219
|
-
writeUnknownMethodHint(stderr, response.error.code);
|
|
1220
|
-
return gatewayCliExitCodes.failure;
|
|
1255
|
+
return reportControlFailure(command, response.error.code, stdout, stderr);
|
|
1221
1256
|
}
|
|
1222
1257
|
if (options.json === true || !isTerminal(stdout)) {
|
|
1223
1258
|
success(response.result);
|
|
@@ -1242,9 +1277,7 @@ export async function runGatewayCli(argv = process.argv.slice(2), dependencies =
|
|
|
1242
1277
|
while (!interrupt.signal.aborted) {
|
|
1243
1278
|
const observed = await ask("observe_snapshot", {});
|
|
1244
1279
|
if (!observed.ok) {
|
|
1245
|
-
|
|
1246
|
-
writeUnknownMethodHint(stderr, observed.error.code);
|
|
1247
|
-
return gatewayCliExitCodes.failure;
|
|
1280
|
+
return reportControlFailure(command, observed.error.code, stdout, stderr);
|
|
1248
1281
|
}
|
|
1249
1282
|
if (observed.result.snapshotRevision !== revision) {
|
|
1250
1283
|
revision = observed.result.snapshotRevision;
|
|
@@ -1274,9 +1307,7 @@ export async function runGatewayCli(argv = process.argv.slice(2), dependencies =
|
|
|
1274
1307
|
if (chosen === undefined) {
|
|
1275
1308
|
const snapshot = await ask("list_snapshot", {});
|
|
1276
1309
|
if (!snapshot.ok) {
|
|
1277
|
-
|
|
1278
|
-
writeUnknownMethodHint(stderr, snapshot.error.code);
|
|
1279
|
-
return gatewayCliExitCodes.failure;
|
|
1310
|
+
return reportControlFailure(command, snapshot.error.code, stdout, stderr);
|
|
1280
1311
|
}
|
|
1281
1312
|
const candidates = snapshot.result.routes
|
|
1282
1313
|
.filter((route) => route.provider === "codex" && route.enabled)
|
|
@@ -1400,9 +1431,7 @@ export async function runGatewayCli(argv = process.argv.slice(2), dependencies =
|
|
|
1400
1431
|
response = await sendRequest({ socketPath: config.controlSocketPath, request });
|
|
1401
1432
|
}
|
|
1402
1433
|
if (!response.ok) {
|
|
1403
|
-
|
|
1404
|
-
writeUnknownMethodHint(stderr, response.error.code);
|
|
1405
|
-
return gatewayCliExitCodes.failure;
|
|
1434
|
+
return reportControlFailure(command, response.error.code, stdout, stderr);
|
|
1406
1435
|
}
|
|
1407
1436
|
if (command === "register-peer" && args.includes("--emit-env") && "token" in response.result) {
|
|
1408
1437
|
stdout.write(`export EMBASSY_PEER_TOKEN='${response.result.token}'\n`);
|