fix: make gateway token recovery usable from macOS onboarding (#118051)

* fix(gateway): add safe token recovery

Co-authored-by: 宇宙熊Yzx <53250620+849261680@users.noreply.github.com>

* chore: move gateway release note to PR

* test(cli): classify gateway token output

---------

Co-authored-by: 宇宙熊Yzx <53250620+849261680@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-08-02 10:06:50 -07:00
committed by GitHub
parent e47f9ba075
commit ad53d4419e
19 changed files with 383 additions and 41 deletions
@@ -10,6 +10,7 @@ const mocks = vi.hoisted(() => ({
emitReachableGatewayAuthDiagnostic: vi.fn(async (_params: unknown) => false),
formatHealthChannelLines: vi.fn(() => []),
gatewayStatusCommand: vi.fn(async (_opts: unknown, _runtime: unknown) => {}),
gatewayAuthTokenCommand: vi.fn(async (_runtime: unknown) => {}),
defaultRuntime: {
log: vi.fn(),
error: vi.fn(),
@@ -46,6 +47,10 @@ vi.mock("../../commands/gateway-status.js", () => ({
mocks.gatewayStatusCommand(opts, runtime),
}));
vi.mock("../../commands/gateway-auth-token.js", () => ({
gatewayAuthTokenCommand: (runtime: unknown) => mocks.gatewayAuthTokenCommand(runtime),
}));
vi.mock("../gateway-rpc.js", async () => ({
...(await vi.importActual<typeof import("../gateway-rpc.js")>("../gateway-rpc.js")),
callGatewayFromCliWithTransport: (method: string, opts: unknown, params?: unknown) =>
@@ -143,6 +148,7 @@ describe("gateway register option collisions", () => {
emitReachableGatewayAuthDiagnostic.mockClear();
mocks.formatHealthChannelLines.mockClear();
gatewayStatusCommand.mockClear();
mocks.gatewayAuthTokenCommand.mockClear();
defaultRuntime.log.mockClear();
defaultRuntime.error.mockClear();
defaultRuntime.writeStdout.mockClear();
@@ -150,6 +156,23 @@ describe("gateway register option collisions", () => {
defaultRuntime.exit.mockClear();
});
it("requires explicit confirmation before revealing the Gateway token", async () => {
await sharedProgram.parseAsync(["gateway", "auth-token"], { from: "user" });
expect(mocks.gatewayAuthTokenCommand).not.toHaveBeenCalled();
expect(defaultRuntime.error).toHaveBeenCalledWith(
expect.stringContaining("Pass --show to confirm"),
);
expect(defaultRuntime.exit).toHaveBeenCalledWith(1);
});
it("routes an explicitly confirmed token reveal through the output runtime", async () => {
await sharedProgram.parseAsync(["gateway", "auth-token", "--show"], { from: "user" });
expect(mocks.gatewayAuthTokenCommand).toHaveBeenCalledWith(defaultRuntime);
expect(defaultRuntime.error).not.toHaveBeenCalled();
});
it.each([
{
name: "forwards --token to gateway call when parent and child option names collide",
+17
View File
@@ -532,6 +532,7 @@ export function registerGatewayCli(program: Command, deps: GatewayCliDependencie
`\n${theme.heading("Examples:")}\n${formatHelpExamples([
["openclaw gateway run", "Run the gateway in the foreground."],
["openclaw gateway status", "Show service status plus connectivity/capability."],
["openclaw gateway auth-token --show", "Reveal the shared token interactively."],
["openclaw gateway discover", "Find local and wide-area gateway beacons."],
["openclaw gateway stability", "Show recent stability diagnostics."],
["openclaw gateway call health", "Call a gateway RPC method directly."],
@@ -549,6 +550,22 @@ export function registerGatewayCli(program: Command, deps: GatewayCliDependencie
addGatewayRestartHandoffCommands(gateway);
setCommandJsonMode(gateway, "output", ({ argv }) => isGatewayMachineOutput(argv));
gateway
.command("auth-token")
.description("Reveal the configured shared Gateway token")
.option("--show", "Print the token to an interactive terminal", false)
.action(async (opts) => {
await runGatewayCommand(async () => {
if (!opts.show) {
throw new Error(
"Pass --show to confirm that you want to print the Gateway token to this terminal.",
);
}
const { gatewayAuthTokenCommand } = await import("../../commands/gateway-auth-token.js");
await gatewayAuthTokenCommand(defaultRuntime);
}, "Gateway auth token failed");
});
gatewayCallOpts(
gateway
.command("call")