fix(signal): validate managed connection ownership

This commit is contained in:
jesse-merhi
2026-08-12 01:21:34 +10:00
parent e2c7edf856
commit dceeee35cf
2 changed files with 68 additions and 21 deletions
@@ -315,26 +315,58 @@ describe("probeManagedSignalSetup", () => {
expect(mocks.spawnDaemon).not.toHaveBeenCalled();
});
it("probes a separate connection URL with selected-account verification", async () => {
it("requires a separate connection URL to become healthy with the managed daemon", async () => {
const candidate = { ...transport, url: "https://signal.example:9443" };
await probeManagedSignalSetup(
createParams(
{
channels: {
signal: {
accounts: {
work: { account, transport: candidate },
},
},
const cfg = {
channels: {
signal: {
accounts: {
work: { account, transport: candidate },
},
},
candidate,
),
);
},
} as OpenClawConfig;
mocks.probeTransport
.mockResolvedValueOnce({ ok: false, error: "connection refused" })
.mockResolvedValueOnce({ ok: true, status: 200 })
.mockResolvedValueOnce({ ok: true, status: 200 });
await expect(probeManagedSignalSetup(createParams(cfg, candidate))).resolves.toMatchObject({
ok: true,
});
expect(mocks.probeTransport).toHaveBeenNthCalledWith(
1,
expect.not.objectContaining({ nativeAccountBinding: "owner-known-bound-account" }),
);
expect(mocks.probeTransport).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ nativeAccountBinding: "owner-known-bound-account" }),
);
expect(mocks.probeTransport).toHaveBeenNthCalledWith(
3,
expect.not.objectContaining({ nativeAccountBinding: "owner-known-bound-account" }),
);
});
it("rejects a separate connection URL already served by an independent daemon", async () => {
const candidate = { ...transport, url: "https://signal.example:9443" };
const cfg = {
channels: {
signal: {
accounts: {
work: { account, transport: candidate },
},
},
},
} as OpenClawConfig;
await expect(probeManagedSignalSetup(createParams(cfg, candidate))).resolves.toMatchObject({
ok: false,
error: expect.stringContaining("external-native"),
});
expect(mocks.probeTransport).toHaveBeenCalledOnce();
expect(mocks.spawnDaemon).not.toHaveBeenCalled();
});
});
@@ -79,6 +79,18 @@ async function probeManagedBind(params: {
return result;
}
function hasSeparateConnectionUrl(params: {
transport: SignalManagedNativeTransport;
resolved: ResolvedManagedSignalTransport;
}): boolean {
return !isSignalManagedNativeConnectionUrlForBind({
...params.transport,
url: params.resolved.baseUrl,
httpHost: params.resolved.httpHost,
httpPort: params.resolved.httpPort,
});
}
async function probeSeparateConnectionUrl(params: {
cfg: OpenClawConfig;
accountId: string;
@@ -88,14 +100,7 @@ async function probeSeparateConnectionUrl(params: {
abortSignal?: AbortSignal;
}): Promise<SignalTransportProbeResult> {
params.abortSignal?.throwIfAborted();
if (
isSignalManagedNativeConnectionUrlForBind({
...params.transport,
url: params.resolved.baseUrl,
httpHost: params.resolved.httpHost,
httpPort: params.resolved.httpPort,
})
) {
if (!hasSeparateConnectionUrl(params)) {
return { ok: true, status: 200, error: null };
}
const result = await probeSignalTransport({
@@ -169,6 +174,16 @@ export async function probeManagedSignalSetup(params: {
httpHost: resolved.httpHost,
httpPort: resolved.httpPort,
});
if (hasSeparateConnectionUrl({ ...params, resolved })) {
const preexistingConnection = await probeSeparateConnectionUrl({ ...params, resolved });
if (preexistingConnection.ok) {
return {
ok: false,
error:
"Signal managed connection URL was already serving the selected account before OpenClaw started its daemon. Use external-native for an independently operated Signal server.",
};
}
}
params.abortSignal?.throwIfAborted();
const spawnedDaemon = spawnSignalDaemon({
cliPath: resolved.cliPath,