Skip to content

Commit f8491b0

Browse files
authored
enhance(slack): route plugin approvals through native UI
Route Slack plugin approval delivery through the shared native approval route gates while preserving Slack Block Kit buttons and plugin resolver semantics. Verification: Slack/native approval unit tests, Slack QA Lab, and live clawd native plugin approval via Slack desktop.
1 parent 98e943e commit f8491b0

11 files changed

Lines changed: 635 additions & 339 deletions

.github/workflows/qa-live-transports-convex.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,7 @@ jobs:
818818
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
819819
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
820820
OPENCLAW_QA_SLACK_CAPTURE_CONTENT: "1"
821+
OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000"
821822
INPUT_SCENARIO: ${{ github.event_name == 'workflow_dispatch' && inputs.slack_scenario || '' }}
822823
run: |
823824
set -euo pipefail

extensions/qa-lab/src/live-transports/slack/slack-live.runtime.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,20 @@ describe("Slack live QA runtime helpers", () => {
256256
).toBe(3_500);
257257
});
258258

259+
it("resolves Slack readiness timeout from the shared transport env", () => {
260+
expect(testing.resolveSlackQaReadyTimeoutMs({})).toBe(45_000);
261+
expect(
262+
testing.resolveSlackQaReadyTimeoutMs({
263+
OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000",
264+
}),
265+
).toBe(180_000);
266+
expect(
267+
testing.resolveSlackQaReadyTimeoutMs({
268+
OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "bad",
269+
}),
270+
).toBe(45_000);
271+
});
272+
259273
it("allows live approval resolve RPCs to take longer than the generic gateway probe timeout", async () => {
260274
const call = vi.fn(async () => ({ decision: "allow-once" }));
261275

extensions/qa-lab/src/live-transports/slack/slack-live.runtime.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ type SlackChannelStatus = {
4949

5050
type SlackChannelReadinessMode = "connected" | "started";
5151

52-
const SLACK_QA_READY_TIMEOUT_MS = 45_000;
52+
const SLACK_QA_DEFAULT_READY_TIMEOUT_MS = 45_000;
5353
const SLACK_QA_READY_STABILITY_MS = 3_000;
5454
const SLACK_QA_GATEWAY_STOP_SETTLE_MS = 3_000;
5555
const SLACK_QA_RETRYABLE_SCENARIO_ATTEMPTS = 2;
@@ -1540,8 +1540,9 @@ async function waitForSlackChannelRunning(
15401540
mode: SlackChannelReadinessMode,
15411541
): Promise<SlackChannelStatus> {
15421542
const startedAt = Date.now();
1543+
const timeoutMs = resolveSlackQaReadyTimeoutMs();
15431544
let lastStatus: SlackChannelStatus | undefined;
1544-
while (Date.now() - startedAt < SLACK_QA_READY_TIMEOUT_MS) {
1545+
while (Date.now() - startedAt < timeoutMs) {
15451546
try {
15461547
const payload = (await gateway.call(
15471548
"channels.status",
@@ -1598,8 +1599,9 @@ async function waitForSlackChannelStable(
15981599
mode: SlackChannelReadinessMode,
15991600
) {
16001601
const startedAt = Date.now();
1602+
const timeoutMs = resolveSlackQaReadyTimeoutMs();
16011603
let readySince: number | undefined;
1602-
while (Date.now() - startedAt < SLACK_QA_READY_TIMEOUT_MS) {
1604+
while (Date.now() - startedAt < timeoutMs) {
16031605
const status = await waitForSlackChannelRunning(gateway, accountId, mode);
16041606
const observedAt = Date.now();
16051607
readySince = resolveSlackChannelReadySince({
@@ -1646,6 +1648,14 @@ function resolveSlackChannelReadySince(params: {
16461648
return params.previousReadySince ?? params.observedAt;
16471649
}
16481650

1651+
function resolveSlackQaReadyTimeoutMs(env: NodeJS.ProcessEnv = process.env) {
1652+
const raw = env.OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS;
1653+
if (!raw) {
1654+
return SLACK_QA_DEFAULT_READY_TIMEOUT_MS;
1655+
}
1656+
return parseStrictPositiveInteger(raw) ?? SLACK_QA_DEFAULT_READY_TIMEOUT_MS;
1657+
}
1658+
16491659
function isRetryableSlackQaScenarioError(error: unknown) {
16501660
return /timed out after \d+ms waiting for Slack message/iu.test(formatErrorMessage(error));
16511661
}
@@ -2167,6 +2177,7 @@ export const testing = {
21672177
parseSlackQaCredentialPayload,
21682178
preserveSlackGatewayDebugArtifacts,
21692179
resolveSlackChannelReadySince,
2180+
resolveSlackQaReadyTimeoutMs,
21702181
resolveSlackApprovalCheckpointConfig,
21712182
resolveApprovalDecision,
21722183
resolveSlackQaRuntimeEnv,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { hasConfiguredAccountValue } from "openclaw/plugin-sdk/account-resolution";
2+
import type { ResolvedSlackAccount } from "./accounts.js";
3+
4+
export function isSlackPluginAccountConfigured(account: ResolvedSlackAccount): boolean {
5+
const mode = account.config.mode ?? "socket";
6+
const hasBotToken = Boolean(account.botToken?.trim());
7+
if (!hasBotToken) {
8+
return false;
9+
}
10+
if (mode === "http") {
11+
return hasConfiguredAccountValue(account.config.signingSecret);
12+
}
13+
return Boolean(account.appToken?.trim());
14+
}

extensions/slack/src/approval-auth.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from "vitest";
2-
import { slackApprovalAuth } from "./approval-auth.js";
2+
import { isSlackApprovalAuthorizedSender, slackApprovalAuth } from "./approval-auth.js";
33

44
describe("slackApprovalAuth", () => {
55
it("authorizes general Slack approvers from allowFrom and defaultTo", () => {
@@ -96,4 +96,29 @@ describe("slackApprovalAuth", () => {
9696
).toEqual({ authorized: true });
9797
}
9898
});
99+
100+
it("allows same-chat plugin approval when no concrete Slack approvers are configured", () => {
101+
const cfg = {
102+
channels: {
103+
slack: {
104+
allowFrom: ["*"],
105+
},
106+
},
107+
};
108+
109+
expect(
110+
slackApprovalAuth.authorizeActorAction({
111+
cfg,
112+
senderId: "U123OWNER",
113+
action: "approve",
114+
approvalKind: "plugin",
115+
}),
116+
).toEqual({ authorized: true });
117+
expect(
118+
isSlackApprovalAuthorizedSender({
119+
cfg,
120+
senderId: "U123OWNER",
121+
}),
122+
).toBe(true);
123+
});
99124
});

extensions/slack/src/approval-auth.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ export function isSlackApprovalAuthorizedSender(params: {
2828
if (!senderId) {
2929
return false;
3030
}
31-
return getSlackApprovalApprovers(params).includes(senderId);
31+
const approvers = getSlackApprovalApprovers(params);
32+
if (approvers.length > 0) {
33+
return approvers.includes(senderId);
34+
}
35+
return (resolveSlackAccountAllowFrom(params) ?? []).some((entry) => entry.trim() === "*");
3236
}
3337

3438
export const slackApprovalAuth = createResolvedApproverActionAuthAdapter({

0 commit comments

Comments
 (0)