Approvals: tag Matrix suppression payloads

This commit is contained in:
Gustavo Madeira Santana 2026-04-02 16:03:11 -04:00
parent 1b992f0bde
commit d9f048e827
8 changed files with 93 additions and 0 deletions

View File

@ -159,6 +159,51 @@ describe("matrix exec approvals", () => {
).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");

View File

@ -117,6 +117,9 @@ export function shouldSuppressLocalMatrixExecApprovalPrompt(params: {
if (!metadata) {
return false;
}
if (metadata.approvalKind !== "exec") {
return false;
}
const request = buildFilterCheckRequest({
metadata,
});

View File

@ -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 });

View File

@ -353,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,
});
}

View File

@ -85,6 +85,7 @@ describe("exec approval reply helpers", () => {
).toEqual({
approvalId: "req-1",
approvalSlug: "slug-1",
approvalKind: "exec",
agentId: "agent-1",
allowedDecisions: ["allow-once", "deny", "allow-always"],
sessionKey: "session-1",
@ -108,6 +109,7 @@ describe("exec approval reply helpers", () => {
execApproval: {
approvalId: "req-1",
approvalSlug: "slug-1",
approvalKind: "exec",
agentId: undefined,
allowedDecisions: ["allow-once", "allow-always", "deny"],
sessionKey: undefined,
@ -157,6 +159,7 @@ describe("exec approval reply helpers", () => {
execApproval: {
approvalId: "req-ask-always",
approvalSlug: "slug-always",
approvalKind: "exec",
allowedDecisions: ["allow-once", "deny"],
},
});
@ -200,6 +203,7 @@ describe("exec approval reply helpers", () => {
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",

View File

@ -15,6 +15,7 @@ export type ExecApprovalUnavailableReason =
export type ExecApprovalReplyMetadata = {
approvalId: string;
approvalSlug: string;
approvalKind: "exec" | "plugin";
agentId?: string;
allowedDecisions?: readonly ExecApprovalReplyDecision[];
sessionKey?: string;
@ -229,6 +230,7 @@ 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 =>
@ -242,6 +244,7 @@ export function getExecApprovalReplyMetadata(
return {
approvalId,
approvalSlug,
approvalKind,
agentId,
allowedDecisions,
sessionKey,
@ -307,6 +310,7 @@ export function buildExecApprovalPendingReplyPayload(
execApproval: {
approvalId: params.approvalId,
approvalSlug: params.approvalSlug,
approvalKind: "exec",
agentId: params.agentId?.trim() || undefined,
allowedDecisions,
sessionKey: params.sessionKey?.trim() || undefined,

View File

@ -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: {

View File

@ -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),