gateway: harden shared auth resolution across systemd, discord, and node host

This commit is contained in:
Josh Avant
2026-03-07 18:28:32 -06:00
committed by GitHub
parent a7f6e0a921
commit 25252ab5ab
28 changed files with 1498 additions and 255 deletions

View File

@@ -10,19 +10,17 @@ type GatewayClientAuth = {
token?: string;
password?: string;
};
type ResolveGatewayCredentialsWithSecretInputs = (params: unknown) => Promise<GatewayClientAuth>;
type ResolveGatewayConnectionAuth = (params: unknown) => Promise<GatewayClientAuth>;
const mockState = {
gateways: [] as MockGatewayClient[],
gatewayAuth: [] as GatewayClientAuth[],
agentSideConnectionCtor: vi.fn(),
agentStart: vi.fn(),
resolveGatewayCredentialsWithSecretInputs: vi.fn<ResolveGatewayCredentialsWithSecretInputs>(
async (_params) => ({
token: undefined,
password: undefined,
}),
),
resolveGatewayConnectionAuth: vi.fn<ResolveGatewayConnectionAuth>(async (_params) => ({
token: undefined,
password: undefined,
})),
};
class MockGatewayClient {
@@ -72,11 +70,22 @@ vi.mock("../gateway/auth.js", () => ({
}));
vi.mock("../gateway/call.js", () => ({
buildGatewayConnectionDetails: () => ({
url: "ws://127.0.0.1:18789",
}),
resolveGatewayCredentialsWithSecretInputs: (params: unknown) =>
mockState.resolveGatewayCredentialsWithSecretInputs(params),
buildGatewayConnectionDetails: ({ url }: { url?: string }) => {
if (typeof url === "string" && url.trim().length > 0) {
return {
url: url.trim(),
urlSource: "cli --url",
};
}
return {
url: "ws://127.0.0.1:18789",
urlSource: "local loopback",
};
},
}));
vi.mock("../gateway/connection-auth.js", () => ({
resolveGatewayConnectionAuth: (params: unknown) => mockState.resolveGatewayConnectionAuth(params),
}));
vi.mock("../gateway/client.js", () => ({
@@ -129,8 +138,8 @@ describe("serveAcpGateway startup", () => {
mockState.gatewayAuth.length = 0;
mockState.agentSideConnectionCtor.mockReset();
mockState.agentStart.mockReset();
mockState.resolveGatewayCredentialsWithSecretInputs.mockReset();
mockState.resolveGatewayCredentialsWithSecretInputs.mockResolvedValue({
mockState.resolveGatewayConnectionAuth.mockReset();
mockState.resolveGatewayConnectionAuth.mockResolvedValue({
token: undefined,
password: undefined,
});
@@ -178,7 +187,7 @@ describe("serveAcpGateway startup", () => {
});
it("passes resolved SecretInput gateway credentials to the ACP gateway client", async () => {
mockState.resolveGatewayCredentialsWithSecretInputs.mockResolvedValue({
mockState.resolveGatewayConnectionAuth.mockResolvedValue({
token: undefined,
password: "resolved-secret-password", // pragma: allowlist secret
});
@@ -188,7 +197,7 @@ describe("serveAcpGateway startup", () => {
const servePromise = serveAcpGateway({});
await Promise.resolve();
expect(mockState.resolveGatewayCredentialsWithSecretInputs).toHaveBeenCalledWith(
expect(mockState.resolveGatewayConnectionAuth).toHaveBeenCalledWith(
expect.objectContaining({
env: process.env,
}),
@@ -209,4 +218,33 @@ describe("serveAcpGateway startup", () => {
onceSpy.mockRestore();
}
});
it("passes CLI URL override context into shared gateway auth resolution", async () => {
const { signalHandlers, onceSpy } = captureProcessSignalHandlers();
try {
const servePromise = serveAcpGateway({
gatewayUrl: "wss://override.example/ws",
});
await Promise.resolve();
expect(mockState.resolveGatewayConnectionAuth).toHaveBeenCalledWith(
expect.objectContaining({
env: process.env,
urlOverride: "wss://override.example/ws",
urlOverrideSource: "cli",
}),
);
const gateway = getMockGateway();
gateway.emitHello();
await vi.waitFor(() => {
expect(mockState.agentSideConnectionCtor).toHaveBeenCalledTimes(1);
});
signalHandlers.get("SIGINT")?.();
await servePromise;
} finally {
onceSpy.mockRestore();
}
});
});

View File

@@ -3,11 +3,9 @@ import { Readable, Writable } from "node:stream";
import { fileURLToPath } from "node:url";
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
import { loadConfig } from "../config/config.js";
import {
buildGatewayConnectionDetails,
resolveGatewayCredentialsWithSecretInputs,
} from "../gateway/call.js";
import { buildGatewayConnectionDetails } from "../gateway/call.js";
import { GatewayClient } from "../gateway/client.js";
import { resolveGatewayConnectionAuth } from "../gateway/connection-auth.js";
import { isMainModule } from "../infra/is-main.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
import { readSecretFromFile } from "./secret-file.js";
@@ -20,13 +18,21 @@ export async function serveAcpGateway(opts: AcpServerOptions = {}): Promise<void
config: cfg,
url: opts.gatewayUrl,
});
const creds = await resolveGatewayCredentialsWithSecretInputs({
const gatewayUrlOverrideSource =
connection.urlSource === "cli --url"
? "cli"
: connection.urlSource === "env OPENCLAW_GATEWAY_URL"
? "env"
: undefined;
const creds = await resolveGatewayConnectionAuth({
config: cfg,
explicitAuth: {
token: opts.gatewayToken,
password: opts.gatewayPassword,
},
env: process.env,
urlOverride: gatewayUrlOverrideSource ? connection.url : undefined,
urlOverrideSource: gatewayUrlOverrideSource,
});
let agent: AcpGatewayAgent | null = null;