mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:10:44 +00:00
fix: quote doctor pairing repair commands (#69210)
This commit is contained in:
@@ -216,6 +216,53 @@ describe("noteDevicePairingHealth", () => {
|
||||
expect(message).not.toContain("control-ui\tclient");
|
||||
});
|
||||
|
||||
it("quotes untrusted device pairing fields in suggested commands", async () => {
|
||||
callGatewayMock.mockResolvedValue({
|
||||
pending: [
|
||||
{
|
||||
requestId: "req-gateway-1",
|
||||
deviceId: "device; echo pwn",
|
||||
publicKey: "pending-pubkey",
|
||||
role: "operator",
|
||||
roles: ["operator"],
|
||||
scopes: ["operator.read"],
|
||||
clientId: "control-ui",
|
||||
clientMode: "webchat",
|
||||
displayName: "Dashboard",
|
||||
ts: 1,
|
||||
isRepair: true,
|
||||
},
|
||||
],
|
||||
paired: [
|
||||
{
|
||||
deviceId: "device; echo pwn",
|
||||
publicKey: "paired-pubkey",
|
||||
displayName: "Dashboard",
|
||||
clientId: "control-ui",
|
||||
clientMode: "webchat",
|
||||
role: "operator; touch /tmp/pwn",
|
||||
roles: ["operator; touch /tmp/pwn"],
|
||||
scopes: [],
|
||||
approvedScopes: [],
|
||||
tokens: [],
|
||||
createdAtMs: 1,
|
||||
approvedAtMs: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await noteDevicePairingHealth({
|
||||
cfg: { gateway: { mode: "remote" } },
|
||||
healthOk: true,
|
||||
});
|
||||
|
||||
const message = String(noteMock.mock.calls[0]?.[0] ?? "");
|
||||
expect(message).toContain("openclaw devices remove 'device; echo pwn'");
|
||||
expect(message).toContain(
|
||||
"openclaw devices rotate --device 'device; echo pwn' --role 'operator; touch /tmp/pwn'",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not duplicate missing-token warnings when local cache exists for an approved role", async () => {
|
||||
await withTempDir("openclaw-doctor-device-pairing-", async (stateDir) => {
|
||||
await withEnvAsync(
|
||||
|
||||
@@ -184,6 +184,17 @@ function formatRoles(roles: string[]): string {
|
||||
return roles.length > 0 ? roles.join(", ") : "none";
|
||||
}
|
||||
|
||||
function quoteCliArg(value: string): string {
|
||||
if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
return `'${value.replaceAll("'", "'\\''")}'`;
|
||||
}
|
||||
|
||||
function formatCliArgs(args: string[]): string {
|
||||
return formatCliCommand(args.map(quoteCliArg).join(" "));
|
||||
}
|
||||
|
||||
function describeDevice(params: {
|
||||
deviceId: string;
|
||||
displayName?: string;
|
||||
@@ -241,8 +252,8 @@ function resolvePendingPairingIssue(
|
||||
displayName: pending.displayName,
|
||||
clientId: pending.clientId,
|
||||
});
|
||||
const approveCommand = formatCliCommand(`openclaw devices approve ${pending.requestId}`);
|
||||
const inspectCommand = formatCliCommand("openclaw devices list");
|
||||
const approveCommand = formatCliArgs(["openclaw", "devices", "approve", pending.requestId]);
|
||||
const inspectCommand = formatCliArgs(["openclaw", "devices", "list"]);
|
||||
if (!paired) {
|
||||
return {
|
||||
kind: "first-time",
|
||||
@@ -259,7 +270,7 @@ function resolvePendingPairingIssue(
|
||||
deviceLabel,
|
||||
approveCommand,
|
||||
inspectCommand,
|
||||
removeCommand: formatCliCommand(`openclaw devices remove ${pending.deviceId}`),
|
||||
removeCommand: formatCliArgs(["openclaw", "devices", "remove", pending.deviceId]),
|
||||
};
|
||||
}
|
||||
const requestedRoles = uniqueStrings(pending.roles, pending.role);
|
||||
@@ -346,9 +357,15 @@ function collectPairedRecordIssues(snapshot: DoctorPairingSnapshot): string[] {
|
||||
}
|
||||
for (const role of approvedRoles) {
|
||||
const token = findTokenSummary(device, role);
|
||||
const rotateCommand = formatCliCommand(
|
||||
`openclaw devices rotate --device ${device.deviceId} --role ${role}`,
|
||||
);
|
||||
const rotateCommand = formatCliArgs([
|
||||
"openclaw",
|
||||
"devices",
|
||||
"rotate",
|
||||
"--device",
|
||||
device.deviceId,
|
||||
"--role",
|
||||
role,
|
||||
]);
|
||||
if (!token) {
|
||||
lines.push(
|
||||
`- Paired device ${deviceLabel} has no active ${role} device token even though the role is approved. This commonly ends in pairing-required or device-token-mismatch. Rotate a fresh token with ${rotateCommand}.`,
|
||||
@@ -456,9 +473,15 @@ function collectLocalDeviceAuthIssues(snapshot: DoctorPairingSnapshot): string[]
|
||||
if (!role) {
|
||||
continue;
|
||||
}
|
||||
const rotateCommand = formatCliCommand(
|
||||
`openclaw devices rotate --device ${paired.deviceId} --role ${role}`,
|
||||
);
|
||||
const rotateCommand = formatCliArgs([
|
||||
"openclaw",
|
||||
"devices",
|
||||
"rotate",
|
||||
"--device",
|
||||
paired.deviceId,
|
||||
"--role",
|
||||
role,
|
||||
]);
|
||||
const pairedToken = findTokenSummary(paired, role);
|
||||
if (!pairedToken) {
|
||||
if (approvedRoles.has(role)) {
|
||||
|
||||
Reference in New Issue
Block a user