mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 13:54:11 +00:00
fix(phone-control): bound arm expiry timestamps
This commit is contained in:
@@ -269,6 +269,25 @@ describe("phone-control plugin", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects arm requests when the expiry would exceed a valid Date", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(8_640_000_000_000_000));
|
||||
try {
|
||||
await withRegisteredPhoneControl(async ({ command, writeConfigFile }) => {
|
||||
const res = await command.handler({
|
||||
...createCommandContext("arm writes 30s"),
|
||||
channel: "webchat",
|
||||
gatewayClientScopes: ["operator.admin"],
|
||||
});
|
||||
|
||||
expect(res?.text ?? "").toContain("Invalid duration");
|
||||
expect(writeConfigFile).not.toHaveBeenCalled();
|
||||
});
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("allows external owner callers without gateway scopes to mutate phone control", async () => {
|
||||
await withRegisteredPhoneControl(async ({ command, writeConfigFile }) => {
|
||||
const res = await command.handler({
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import {
|
||||
asDateTimestampMs,
|
||||
resolveExpiresAtMsFromDurationMs,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import { replaceFileAtomic } from "openclaw/plugin-sdk/security-runtime";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
@@ -294,14 +298,38 @@ function lacksAdminToMutatePhoneControl(params: {
|
||||
return senderIsOwner !== true;
|
||||
}
|
||||
|
||||
function resolveArmExpiryStatus(state: ArmStateFile, nowRaw = Date.now()): string {
|
||||
if (state.expiresAtMs == null) {
|
||||
return "manual disarm required";
|
||||
}
|
||||
const now = asDateTimestampMs(nowRaw);
|
||||
if (now === undefined) {
|
||||
return "expiry unavailable";
|
||||
}
|
||||
const expiresAt = asDateTimestampMs(state.expiresAtMs);
|
||||
if (expiresAt === undefined || expiresAt <= now) {
|
||||
return "expired";
|
||||
}
|
||||
return `expires in ${formatDuration(expiresAt - now)}`;
|
||||
}
|
||||
|
||||
function isArmStateExpired(state: ArmStateFile, nowRaw = Date.now()): boolean {
|
||||
if (state.expiresAtMs == null) {
|
||||
return false;
|
||||
}
|
||||
const now = asDateTimestampMs(nowRaw);
|
||||
if (now === undefined) {
|
||||
return false;
|
||||
}
|
||||
const expiresAt = asDateTimestampMs(state.expiresAtMs);
|
||||
return expiresAt === undefined || expiresAt <= now;
|
||||
}
|
||||
|
||||
function formatStatus(state: ArmStateFile | null): string {
|
||||
if (!state) {
|
||||
return "Phone control: disarmed.";
|
||||
}
|
||||
const until =
|
||||
state.expiresAtMs == null
|
||||
? "manual disarm required"
|
||||
: `expires in ${formatDuration(Math.max(0, state.expiresAtMs - Date.now()))}`;
|
||||
const until = resolveArmExpiryStatus(state);
|
||||
const cmds = uniqSorted(
|
||||
state.version === 1
|
||||
? state.removedFromDeny
|
||||
@@ -329,7 +357,7 @@ export default definePluginEntry({
|
||||
if (!state || state.expiresAtMs == null) {
|
||||
return;
|
||||
}
|
||||
if (Date.now() < state.expiresAtMs) {
|
||||
if (!isArmStateExpired(state)) {
|
||||
return;
|
||||
}
|
||||
await disarmNow({
|
||||
@@ -430,7 +458,14 @@ export default definePluginEntry({
|
||||
if (durationMs === null) {
|
||||
return { text: "Invalid duration. Use values like 30s, 10m, 2h, or 1d." };
|
||||
}
|
||||
const expiresAtMs = Date.now() + durationMs;
|
||||
const armedAtMs = asDateTimestampMs(Date.now());
|
||||
const expiresAtMs =
|
||||
armedAtMs === undefined
|
||||
? undefined
|
||||
: resolveExpiresAtMsFromDurationMs(durationMs, { nowMs: armedAtMs });
|
||||
if (armedAtMs === undefined || expiresAtMs === undefined) {
|
||||
return { text: "Invalid duration. Use values like 30s, 10m, 2h, or 1d." };
|
||||
}
|
||||
|
||||
const commands = resolveCommandsForGroup(group);
|
||||
const cfg = api.runtime.config.current() as OpenClawConfig;
|
||||
@@ -461,7 +496,7 @@ export default definePluginEntry({
|
||||
|
||||
await writeArmState(statePath, {
|
||||
version: STATE_VERSION,
|
||||
armedAtMs: Date.now(),
|
||||
armedAtMs,
|
||||
expiresAtMs,
|
||||
group,
|
||||
armedCommands: uniqSorted(commands),
|
||||
|
||||
Reference in New Issue
Block a user