mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 01:30:21 +00:00
Matrix: add native exec approvals (#58635)
Merged via squash.
Prepared head SHA: d9f048e827
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
committed by
GitHub
parent
9e2cbf9a30
commit
1efa923ab8
@@ -512,6 +512,7 @@ export type ChannelApprovalDeliveryAdapter = {
|
||||
hasConfiguredDmRoute?: (params: { cfg: OpenClawConfig }) => boolean;
|
||||
shouldSuppressForwardingFallback?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
approvalKind: ChannelApprovalKind;
|
||||
target: ChannelApprovalForwardTarget;
|
||||
request: ExecApprovalRequest;
|
||||
}) => boolean;
|
||||
|
||||
@@ -483,6 +483,31 @@ describe("exec approval forwarder", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("stores exec metadata on generic forwarded fallback payloads", async () => {
|
||||
vi.useFakeTimers();
|
||||
const { deliver, forwarder } = createForwarder({ cfg: TARGETS_CFG });
|
||||
|
||||
await expect(forwarder.handleRequested(baseRequest)).resolves.toBe(true);
|
||||
|
||||
expect(deliver).toHaveBeenCalledTimes(1);
|
||||
expect(deliver.mock.calls[0]?.[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
payloads: [
|
||||
expect.objectContaining({
|
||||
channelData: expect.objectContaining({
|
||||
execApproval: expect.objectContaining({
|
||||
approvalId: "req-1",
|
||||
approvalKind: "exec",
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("formats single-line commands as inline code", async () => {
|
||||
vi.useFakeTimers();
|
||||
const { deliver, forwarder } = createForwarder({ cfg: TARGETS_CFG });
|
||||
|
||||
@@ -169,6 +169,7 @@ function buildSyntheticApprovalRequest(routeRequest: ApprovalRouteRequest): Exec
|
||||
}
|
||||
|
||||
function shouldSkipForwardingFallback(params: {
|
||||
approvalKind: "exec" | "plugin";
|
||||
target: ExecApprovalForwardTarget;
|
||||
cfg: OpenClawConfig;
|
||||
routeRequest: ApprovalRouteRequest;
|
||||
@@ -181,6 +182,7 @@ function shouldSkipForwardingFallback(params: {
|
||||
return (
|
||||
adapter?.delivery?.shouldSuppressForwardingFallback?.({
|
||||
cfg: params.cfg,
|
||||
approvalKind: params.approvalKind,
|
||||
target: params.target,
|
||||
request: buildSyntheticApprovalRequest(params.routeRequest),
|
||||
}) ?? false
|
||||
@@ -351,7 +353,9 @@ function buildExecPendingPayload(params: {
|
||||
approvalId: params.request.id,
|
||||
approvalSlug: params.request.id.slice(0, 8),
|
||||
text: buildRequestMessage(params.request, params.nowMs),
|
||||
agentId: params.request.request.agentId ?? null,
|
||||
allowedDecisions: resolveExecApprovalRequestAllowedDecisions(params.request.request),
|
||||
sessionKey: params.request.request.sessionKey ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -502,8 +506,15 @@ function createApprovalHandlers<
|
||||
resolveSessionTarget: params.resolveSessionTarget,
|
||||
})
|
||||
: []),
|
||||
].filter((target) => !shouldSkipForwardingFallback({ target, cfg, routeRequest }));
|
||||
|
||||
].filter(
|
||||
(target) =>
|
||||
!shouldSkipForwardingFallback({
|
||||
approvalKind: params.strategy.kind,
|
||||
target,
|
||||
cfg,
|
||||
routeRequest,
|
||||
}),
|
||||
);
|
||||
if (filteredTargets.length === 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -598,7 +609,15 @@ function createApprovalHandlers<
|
||||
resolveSessionTarget: params.resolveSessionTarget,
|
||||
})
|
||||
: []),
|
||||
].filter((target) => !shouldSkipForwardingFallback({ target, cfg, routeRequest }));
|
||||
].filter(
|
||||
(target) =>
|
||||
!shouldSkipForwardingFallback({
|
||||
approvalKind: params.strategy.kind,
|
||||
target,
|
||||
cfg,
|
||||
routeRequest,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!targets?.length) {
|
||||
|
||||
@@ -76,14 +76,19 @@ describe("exec approval reply helpers", () => {
|
||||
execApproval: {
|
||||
approvalId: " req-1 ",
|
||||
approvalSlug: " slug-1 ",
|
||||
agentId: " agent-1 ",
|
||||
allowedDecisions: ["allow-once", "bad", "deny", "allow-always", 3],
|
||||
sessionKey: " session-1 ",
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
approvalId: "req-1",
|
||||
approvalSlug: "slug-1",
|
||||
approvalKind: "exec",
|
||||
agentId: "agent-1",
|
||||
allowedDecisions: ["allow-once", "deny", "allow-always"],
|
||||
sessionKey: "session-1",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,7 +109,10 @@ describe("exec approval reply helpers", () => {
|
||||
execApproval: {
|
||||
approvalId: "req-1",
|
||||
approvalSlug: "slug-1",
|
||||
approvalKind: "exec",
|
||||
agentId: undefined,
|
||||
allowedDecisions: ["allow-once", "allow-always", "deny"],
|
||||
sessionKey: undefined,
|
||||
},
|
||||
});
|
||||
expect(payload.interactive).toEqual({
|
||||
@@ -151,6 +159,7 @@ describe("exec approval reply helpers", () => {
|
||||
execApproval: {
|
||||
approvalId: "req-ask-always",
|
||||
approvalSlug: "slug-always",
|
||||
approvalKind: "exec",
|
||||
allowedDecisions: ["allow-once", "deny"],
|
||||
},
|
||||
});
|
||||
@@ -180,6 +189,28 @@ describe("exec approval reply helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("stores agent and session metadata for downstream suppression checks", () => {
|
||||
const payload = buildExecApprovalPendingReplyPayload({
|
||||
approvalId: "req-meta",
|
||||
approvalSlug: "slug-meta",
|
||||
agentId: "ops-agent",
|
||||
sessionKey: "agent:ops-agent:matrix:channel:!room:example.org",
|
||||
command: "echo ok",
|
||||
host: "gateway",
|
||||
});
|
||||
|
||||
expect(payload.channelData).toEqual({
|
||||
execApproval: {
|
||||
approvalId: "req-meta",
|
||||
approvalSlug: "slug-meta",
|
||||
approvalKind: "exec",
|
||||
agentId: "ops-agent",
|
||||
allowedDecisions: ["allow-once", "allow-always", "deny"],
|
||||
sessionKey: "agent:ops-agent:matrix:channel:!room:example.org",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("uses a longer fence for commands containing triple backticks", () => {
|
||||
const payload = buildExecApprovalPendingReplyPayload({
|
||||
approvalId: "req-2",
|
||||
|
||||
@@ -15,7 +15,10 @@ export type ExecApprovalUnavailableReason =
|
||||
export type ExecApprovalReplyMetadata = {
|
||||
approvalId: string;
|
||||
approvalSlug: string;
|
||||
approvalKind: "exec" | "plugin";
|
||||
agentId?: string;
|
||||
allowedDecisions?: readonly ExecApprovalReplyDecision[];
|
||||
sessionKey?: string;
|
||||
};
|
||||
|
||||
export type ExecApprovalActionDescriptor = {
|
||||
@@ -31,11 +34,13 @@ export type ExecApprovalPendingReplyParams = {
|
||||
approvalSlug: string;
|
||||
approvalCommandId?: string;
|
||||
ask?: string | null;
|
||||
agentId?: string | null;
|
||||
allowedDecisions?: readonly ExecApprovalReplyDecision[];
|
||||
command: string;
|
||||
cwd?: string;
|
||||
host: ExecHost;
|
||||
nodeId?: string;
|
||||
sessionKey?: string | null;
|
||||
expiresAtMs?: number;
|
||||
nowMs?: number;
|
||||
};
|
||||
@@ -225,16 +230,24 @@ export function getExecApprovalReplyMetadata(
|
||||
if (!approvalId || !approvalSlug) {
|
||||
return null;
|
||||
}
|
||||
const approvalKind = record.approvalKind === "plugin" ? "plugin" : "exec";
|
||||
const allowedDecisions = Array.isArray(record.allowedDecisions)
|
||||
? record.allowedDecisions.filter(
|
||||
(value): value is ExecApprovalReplyDecision =>
|
||||
value === "allow-once" || value === "allow-always" || value === "deny",
|
||||
)
|
||||
: undefined;
|
||||
const agentId =
|
||||
typeof record.agentId === "string" ? record.agentId.trim() || undefined : undefined;
|
||||
const sessionKey =
|
||||
typeof record.sessionKey === "string" ? record.sessionKey.trim() || undefined : undefined;
|
||||
return {
|
||||
approvalId,
|
||||
approvalSlug,
|
||||
approvalKind,
|
||||
agentId,
|
||||
allowedDecisions,
|
||||
sessionKey,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -297,7 +310,10 @@ export function buildExecApprovalPendingReplyPayload(
|
||||
execApproval: {
|
||||
approvalId: params.approvalId,
|
||||
approvalSlug: params.approvalSlug,
|
||||
approvalKind: "exec",
|
||||
agentId: params.agentId?.trim() || undefined,
|
||||
allowedDecisions,
|
||||
sessionKey: params.sessionKey?.trim() || undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -165,6 +165,7 @@ describe("createApproverRestrictedNativeApprovalAdapter", () => {
|
||||
expect(
|
||||
shouldSuppressForwardingFallback({
|
||||
cfg: {} as never,
|
||||
approvalKind: "exec",
|
||||
target: { channel: "telegram", to: "target-1" },
|
||||
request: {
|
||||
request: {
|
||||
@@ -179,6 +180,7 @@ describe("createApproverRestrictedNativeApprovalAdapter", () => {
|
||||
expect(
|
||||
shouldSuppressForwardingFallback({
|
||||
cfg: {} as never,
|
||||
approvalKind: "exec",
|
||||
target: { channel: "telegram", to: "target-1" },
|
||||
request: {
|
||||
request: {
|
||||
@@ -193,6 +195,7 @@ describe("createApproverRestrictedNativeApprovalAdapter", () => {
|
||||
expect(
|
||||
shouldSuppressForwardingFallback({
|
||||
cfg: {} as never,
|
||||
approvalKind: "exec",
|
||||
target: { channel: "slack", to: "target-1" },
|
||||
request: {
|
||||
request: {
|
||||
@@ -208,6 +211,21 @@ describe("createApproverRestrictedNativeApprovalAdapter", () => {
|
||||
cfg: {} as never,
|
||||
accountId: "topic-1",
|
||||
});
|
||||
|
||||
expect(
|
||||
shouldSuppressForwardingFallback({
|
||||
cfg: {} as never,
|
||||
approvalKind: "plugin",
|
||||
target: { channel: "telegram", to: "target-1" },
|
||||
request: {
|
||||
request: {
|
||||
command: "pwd",
|
||||
turnSourceChannel: "telegram",
|
||||
turnSourceAccountId: "topic-1",
|
||||
},
|
||||
} as never,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ type ApprovalAdapterParams = {
|
||||
|
||||
type DeliverySuppressionParams = {
|
||||
cfg: OpenClawConfig;
|
||||
approvalKind: ApprovalKind;
|
||||
target: { channel: string; accountId?: string | null };
|
||||
request: { request: { turnSourceChannel?: string | null; turnSourceAccountId?: string | null } };
|
||||
};
|
||||
|
||||
@@ -89,9 +89,12 @@ describe("plugin-sdk/approval-renderers", () => {
|
||||
},
|
||||
channelDataExpected: {
|
||||
execApproval: {
|
||||
agentId: undefined,
|
||||
approvalId: "plugin-approval-123",
|
||||
approvalKind: "plugin",
|
||||
approvalSlug: "custom-slug",
|
||||
allowedDecisions: ["allow-once", "allow-always", "deny"],
|
||||
sessionKey: undefined,
|
||||
state: "pending",
|
||||
},
|
||||
telegram: {
|
||||
|
||||
@@ -13,10 +13,13 @@ import {
|
||||
const DEFAULT_ALLOWED_DECISIONS = ["allow-once", "allow-always", "deny"] as const;
|
||||
|
||||
export function buildApprovalPendingReplyPayload(params: {
|
||||
approvalKind?: "exec" | "plugin";
|
||||
approvalId: string;
|
||||
approvalSlug: string;
|
||||
text: string;
|
||||
agentId?: string | null;
|
||||
allowedDecisions?: readonly ExecApprovalReplyDecision[];
|
||||
sessionKey?: string | null;
|
||||
channelData?: Record<string, unknown>;
|
||||
}): ReplyPayload {
|
||||
const allowedDecisions = params.allowedDecisions ?? DEFAULT_ALLOWED_DECISIONS;
|
||||
@@ -30,7 +33,10 @@ export function buildApprovalPendingReplyPayload(params: {
|
||||
execApproval: {
|
||||
approvalId: params.approvalId,
|
||||
approvalSlug: params.approvalSlug,
|
||||
approvalKind: params.approvalKind ?? "exec",
|
||||
agentId: params.agentId?.trim() || undefined,
|
||||
allowedDecisions,
|
||||
sessionKey: params.sessionKey?.trim() || undefined,
|
||||
state: "pending",
|
||||
},
|
||||
...params.channelData,
|
||||
@@ -66,6 +72,7 @@ export function buildPluginApprovalPendingReplyPayload(params: {
|
||||
channelData?: Record<string, unknown>;
|
||||
}): ReplyPayload {
|
||||
return buildApprovalPendingReplyPayload({
|
||||
approvalKind: "plugin",
|
||||
approvalId: params.request.id,
|
||||
approvalSlug: params.approvalSlug ?? params.request.id.slice(0, 8),
|
||||
text: params.text ?? buildPluginApprovalRequestMessage(params.request, params.nowMs),
|
||||
|
||||
Reference in New Issue
Block a user