mirror of https://github.com/openclaw/openclaw.git
458 lines
12 KiB
TypeScript
458 lines
12 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
getMatrixExecApprovalApprovers,
|
|
isMatrixExecApprovalApprover,
|
|
isMatrixExecApprovalAuthorizedSender,
|
|
isMatrixExecApprovalClientEnabled,
|
|
isMatrixExecApprovalTargetRecipient,
|
|
normalizeMatrixApproverId,
|
|
resolveMatrixExecApprovalTarget,
|
|
shouldHandleMatrixExecApprovalRequest,
|
|
shouldSuppressLocalMatrixExecApprovalPrompt,
|
|
} from "./exec-approvals.js";
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs.splice(0)) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
function createTempDir(): string {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-exec-approvals-"));
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
function buildConfig(
|
|
execApprovals?: NonNullable<NonNullable<OpenClawConfig["channels"]>["matrix"]>["execApprovals"],
|
|
channelOverrides?: Partial<NonNullable<NonNullable<OpenClawConfig["channels"]>["matrix"]>>,
|
|
): OpenClawConfig {
|
|
return {
|
|
channels: {
|
|
matrix: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot:example.org",
|
|
accessToken: "tok",
|
|
...channelOverrides,
|
|
execApprovals,
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
}
|
|
|
|
describe("matrix exec approvals", () => {
|
|
it("requires enablement and an explicit or inferred approver", () => {
|
|
expect(isMatrixExecApprovalClientEnabled({ cfg: buildConfig() })).toBe(false);
|
|
expect(
|
|
isMatrixExecApprovalClientEnabled({
|
|
cfg: buildConfig(undefined, { dm: { allowFrom: ["@owner:example.org"] } }),
|
|
}),
|
|
).toBe(false);
|
|
expect(isMatrixExecApprovalClientEnabled({ cfg: buildConfig({ enabled: true }) })).toBe(false);
|
|
expect(
|
|
isMatrixExecApprovalClientEnabled({
|
|
cfg: buildConfig({ enabled: true }, { dm: { allowFrom: ["@owner:example.org"] } }),
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isMatrixExecApprovalClientEnabled({
|
|
cfg: buildConfig({ enabled: true, approvers: ["@owner:example.org"] }),
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("prefers explicit approvers when configured", () => {
|
|
const cfg = buildConfig(
|
|
{ enabled: true, approvers: ["user:@override:example.org"] },
|
|
{ dm: { allowFrom: ["@owner:example.org"] } },
|
|
);
|
|
|
|
expect(getMatrixExecApprovalApprovers({ cfg })).toEqual(["@override:example.org"]);
|
|
expect(isMatrixExecApprovalApprover({ cfg, senderId: "@override:example.org" })).toBe(true);
|
|
expect(isMatrixExecApprovalApprover({ cfg, senderId: "@owner:example.org" })).toBe(false);
|
|
});
|
|
|
|
it("ignores wildcard allowlist entries when inferring exec approvers", () => {
|
|
const cfg = buildConfig({ enabled: true }, { dm: { allowFrom: ["*"] } });
|
|
|
|
expect(getMatrixExecApprovalApprovers({ cfg })).toEqual([]);
|
|
expect(isMatrixExecApprovalClientEnabled({ cfg })).toBe(false);
|
|
});
|
|
|
|
it("defaults target to dm", () => {
|
|
expect(
|
|
resolveMatrixExecApprovalTarget({
|
|
cfg: buildConfig({ enabled: true, approvers: ["@owner:example.org"] }),
|
|
}),
|
|
).toBe("dm");
|
|
});
|
|
|
|
it("matches matrix target recipients from generic approval forwarding targets", () => {
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot:example.org",
|
|
accessToken: "tok",
|
|
},
|
|
},
|
|
approvals: {
|
|
exec: {
|
|
enabled: true,
|
|
mode: "targets",
|
|
targets: [
|
|
{ channel: "matrix", to: "user:@target:example.org" },
|
|
{ channel: "matrix", to: "room:!ops:example.org" },
|
|
],
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
|
|
expect(isMatrixExecApprovalTargetRecipient({ cfg, senderId: "@target:example.org" })).toBe(
|
|
true,
|
|
);
|
|
expect(isMatrixExecApprovalTargetRecipient({ cfg, senderId: "@other:example.org" })).toBe(
|
|
false,
|
|
);
|
|
expect(isMatrixExecApprovalAuthorizedSender({ cfg, senderId: "@target:example.org" })).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("suppresses local prompts only when the native client is enabled", () => {
|
|
const payload = {
|
|
channelData: {
|
|
execApproval: {
|
|
approvalId: "req-1",
|
|
approvalSlug: "req-1",
|
|
agentId: "ops-agent",
|
|
sessionKey: "agent:ops-agent:matrix:channel:!ops:example.org",
|
|
},
|
|
},
|
|
};
|
|
|
|
expect(
|
|
shouldSuppressLocalMatrixExecApprovalPrompt({
|
|
cfg: buildConfig({ enabled: true, approvers: ["@owner:example.org"] }),
|
|
payload,
|
|
}),
|
|
).toBe(true);
|
|
|
|
expect(
|
|
shouldSuppressLocalMatrixExecApprovalPrompt({
|
|
cfg: buildConfig(),
|
|
payload,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("keeps local prompts when filters exclude the request", () => {
|
|
const payload = {
|
|
channelData: {
|
|
execApproval: {
|
|
approvalId: "req-1",
|
|
approvalSlug: "req-1",
|
|
agentId: "other-agent",
|
|
sessionKey: "agent:other-agent:matrix:channel:!ops:example.org",
|
|
},
|
|
},
|
|
};
|
|
|
|
expect(
|
|
shouldSuppressLocalMatrixExecApprovalPrompt({
|
|
cfg: buildConfig({
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
agentFilter: ["ops-agent"],
|
|
}),
|
|
payload,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("suppresses local prompts for generic exec payloads when metadata matches filters", () => {
|
|
const payload = {
|
|
channelData: {
|
|
execApproval: {
|
|
approvalId: "req-1",
|
|
approvalSlug: "req-1",
|
|
approvalKind: "exec",
|
|
agentId: "ops-agent",
|
|
sessionKey: "agent:ops-agent:matrix:channel:!ops:example.org",
|
|
},
|
|
},
|
|
};
|
|
|
|
expect(
|
|
shouldSuppressLocalMatrixExecApprovalPrompt({
|
|
cfg: buildConfig({
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
agentFilter: ["ops-agent"],
|
|
sessionFilter: ["matrix:channel:"],
|
|
}),
|
|
payload,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("does not suppress local prompts for plugin approval payloads", () => {
|
|
const payload = {
|
|
channelData: {
|
|
execApproval: {
|
|
approvalId: "plugin:req-1",
|
|
approvalSlug: "plugin:r",
|
|
approvalKind: "plugin",
|
|
},
|
|
},
|
|
};
|
|
|
|
expect(
|
|
shouldSuppressLocalMatrixExecApprovalPrompt({
|
|
cfg: buildConfig({ enabled: true, approvers: ["@owner:example.org"] }),
|
|
payload,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("normalizes prefixed approver ids", () => {
|
|
expect(normalizeMatrixApproverId("matrix:@owner:example.org")).toBe("@owner:example.org");
|
|
expect(normalizeMatrixApproverId("user:@owner:example.org")).toBe("@owner:example.org");
|
|
});
|
|
|
|
it("applies agent and session filters to request handling", () => {
|
|
const cfg = buildConfig({
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
agentFilter: ["ops-agent"],
|
|
sessionFilter: ["matrix:channel:", "ops$"],
|
|
});
|
|
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
request: {
|
|
id: "req-1",
|
|
request: {
|
|
command: "echo hi",
|
|
agentId: "ops-agent",
|
|
sessionKey: "agent:ops-agent:matrix:channel:!room:example.org:ops",
|
|
},
|
|
createdAtMs: 0,
|
|
expiresAtMs: 1000,
|
|
},
|
|
}),
|
|
).toBe(true);
|
|
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
request: {
|
|
id: "req-2",
|
|
request: {
|
|
command: "echo hi",
|
|
agentId: "other-agent",
|
|
sessionKey: "agent:other-agent:matrix:channel:!room:example.org:ops",
|
|
},
|
|
createdAtMs: 0,
|
|
expiresAtMs: 1000,
|
|
},
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("scopes non-matrix turn sources to the stored matrix account", () => {
|
|
const tmpDir = createTempDir();
|
|
const storePath = path.join(tmpDir, "sessions.json");
|
|
fs.writeFileSync(
|
|
storePath,
|
|
JSON.stringify({
|
|
"agent:ops-agent:matrix:channel:!room:example.org": {
|
|
sessionId: "main",
|
|
updatedAt: 1,
|
|
origin: {
|
|
provider: "matrix",
|
|
accountId: "ops",
|
|
},
|
|
lastChannel: "slack",
|
|
lastTo: "channel:C999",
|
|
lastAccountId: "work",
|
|
},
|
|
}),
|
|
"utf-8",
|
|
);
|
|
const cfg = {
|
|
session: { store: storePath },
|
|
channels: {
|
|
matrix: {
|
|
accounts: {
|
|
default: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot-default:example.org",
|
|
accessToken: "tok-default",
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
},
|
|
},
|
|
ops: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot-ops:example.org",
|
|
accessToken: "tok-ops",
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const request = {
|
|
id: "req-3",
|
|
request: {
|
|
command: "echo hi",
|
|
agentId: "ops-agent",
|
|
sessionKey: "agent:ops-agent:matrix:channel:!room:example.org",
|
|
turnSourceChannel: "slack",
|
|
turnSourceTo: "channel:C123",
|
|
},
|
|
createdAtMs: 0,
|
|
expiresAtMs: 1000,
|
|
};
|
|
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
accountId: "default",
|
|
request,
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
accountId: "ops",
|
|
request,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("rejects unbound foreign-channel approvals in multi-account matrix configs", () => {
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
accounts: {
|
|
default: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot-default:example.org",
|
|
accessToken: "tok-default",
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
},
|
|
},
|
|
ops: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot-ops:example.org",
|
|
accessToken: "tok-ops",
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const request = {
|
|
id: "req-4",
|
|
request: {
|
|
command: "echo hi",
|
|
agentId: "ops-agent",
|
|
sessionKey: "agent:ops-agent:missing",
|
|
turnSourceChannel: "slack",
|
|
turnSourceTo: "channel:C123",
|
|
},
|
|
createdAtMs: 0,
|
|
expiresAtMs: 1000,
|
|
};
|
|
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
accountId: "default",
|
|
request,
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
accountId: "ops",
|
|
request,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("allows unbound foreign-channel approvals when only one matrix account can handle them", () => {
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
accounts: {
|
|
default: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot-default:example.org",
|
|
accessToken: "tok-default",
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["@owner:example.org"],
|
|
},
|
|
},
|
|
ops: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot-ops:example.org",
|
|
accessToken: "tok-ops",
|
|
execApprovals: {
|
|
enabled: false,
|
|
approvers: ["@owner:example.org"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const request = {
|
|
id: "req-5",
|
|
request: {
|
|
command: "echo hi",
|
|
agentId: "ops-agent",
|
|
sessionKey: "agent:ops-agent:missing",
|
|
turnSourceChannel: "slack",
|
|
turnSourceTo: "channel:C123",
|
|
},
|
|
createdAtMs: 0,
|
|
expiresAtMs: 1000,
|
|
};
|
|
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
accountId: "default",
|
|
request,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
shouldHandleMatrixExecApprovalRequest({
|
|
cfg,
|
|
accountId: "ops",
|
|
request,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
});
|