Skip to content

Commit 3f3eb57

Browse files
committed
Merge remote-tracking branch 'upstream/main' into codex/99885-git-install-exdev
2 parents 90f515b + 46ad3f9 commit 3f3eb57

36 files changed

Lines changed: 389 additions & 596 deletions
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
729181bf726ea51bebfa51c760eff8d5016933c21815e47197b5d05db8d549d5 plugin-sdk-api-baseline.json
2-
d8add92c9c5445e8bdd8a8361cdd5a42e1ab5baa45cd16ea640152d6f577f1bf plugin-sdk-api-baseline.jsonl
1+
30f5e0b6532e492da594cc5671abb625de279908a34804d0a481a0c77c3a511d plugin-sdk-api-baseline.json
2+
ffa22f0d52fc1d6e55e8db42d09b16490822343b0d9a2d2d60169633e5bce580 plugin-sdk-api-baseline.jsonl

docs/plugins/reference/codex.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ OpenClaw Codex app-server harness and model provider plugin with a Codex-managed
1616

1717
## Surface
1818

19-
providers: codex; contracts: mediaUnderstandingProviders, migrationProviders, webSearchProviders
19+
providers: codex; contracts: mediaUnderstandingProviders, migrationProviders, tools, webSearchProviders
2020

2121
## Related docs
2222

docs/plugins/reference/whatsapp.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ OpenClaw WhatsApp channel plugin for WhatsApp Web chats.
1616

1717
## Surface
1818

19-
channels: whatsapp; skills
19+
channels: whatsapp; contracts: tools; skills
2020

2121
## Related docs
2222

extensions/slack/src/channel.test.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -433,20 +433,24 @@ describe("slackPlugin status", () => {
433433
const cfg = {
434434
channels: {
435435
slack: {
436-
botToken: "xoxb-test",
437-
appToken: "xapp-test",
436+
accounts: {
437+
work: {
438+
botToken: "xoxb-work",
439+
appToken: "xapp-work",
440+
},
441+
},
438442
},
439443
},
440444
} as OpenClawConfig;
441-
const account = slackPlugin.config.resolveAccount(cfg, "default");
445+
const account = slackPlugin.config.resolveAccount(cfg, "work");
442446

443447
const result = await slackPlugin.status!.probeAccount!({
444448
account,
445449
timeoutMs: 2500,
446450
cfg,
447451
});
448452

449-
expect(probeSpy).toHaveBeenCalledWith("xoxb-test", 2500);
453+
expect(probeSpy).toHaveBeenCalledWith("xoxb-work", 2500, { accountId: "work" });
450454
expect(result).toEqual({
451455
ok: true,
452456
status: 200,
@@ -455,6 +459,26 @@ describe("slackPlugin status", () => {
455459
});
456460
});
457461

462+
it("renders Slack probe token warnings in capabilities output", () => {
463+
const lines = slackPlugin.status?.formatCapabilitiesProbe?.({
464+
probe: {
465+
ok: true,
466+
warning: "Slack bot token is a user token",
467+
bot: { id: "UUSER", name: "human-installer" },
468+
team: { id: "T1", name: "OpenClaw" },
469+
},
470+
});
471+
472+
expect(lines).toStrictEqual([
473+
{
474+
text: "Warning: Slack bot token is a user token",
475+
tone: "warn",
476+
},
477+
{ text: "Bot: @human-installer" },
478+
{ text: "Team: OpenClaw (T1)" },
479+
]);
480+
});
481+
458482
it("recovers thread routing from mixed-case Slack session keys", async () => {
459483
const resolveRoute = slackPlugin.messaging?.resolveOutboundSessionRoute;
460484
if (!resolveRoute) {

extensions/slack/src/channel.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -687,11 +687,18 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
687687
if (!token) {
688688
return { ok: false, error: "missing token" };
689689
}
690-
return await (await loadSlackProbeModule()).probeSlack(token, timeoutMs);
690+
return await (
691+
await loadSlackProbeModule()
692+
).probeSlack(token, timeoutMs, {
693+
accountId: account.accountId,
694+
});
691695
},
692696
formatCapabilitiesProbe: ({ probe }) => {
693697
const slackProbe = probe as SlackProbe | undefined;
694698
const lines = [];
699+
if (slackProbe?.warning) {
700+
lines.push({ text: `Warning: ${slackProbe.warning}`, tone: "warn" } as const);
701+
}
695702
if (slackProbe?.bot?.name) {
696703
lines.push({ text: `Bot: @${slackProbe.bot.name}` });
697704
}

extensions/slack/src/monitor.test-helpers.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Slack helper module supports monitor helpers behavior.
22
import type { ChannelRuntimeSurface } from "openclaw/plugin-sdk/channel-contract";
3+
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
34
import { vi } from "vitest";
45
import type { Mock } from "vitest";
56
import { clearSlackInboundDeliveryStateForTest } from "./monitor/inbound-delivery-state.js";
@@ -12,6 +13,7 @@ type SlackProviderMonitor = (params: {
1213
abortSignal: AbortSignal;
1314
config?: Record<string, unknown>;
1415
channelRuntime?: ChannelRuntimeSurface;
16+
runtime?: RuntimeEnv;
1517
}) => Promise<unknown>;
1618

1719
type SlackTestState = {
@@ -86,7 +88,7 @@ function ensureSlackTestRuntime(): {
8688
}
8789
if (!globalState["__slackClient"]) {
8890
globalState["__slackClient"] = {
89-
auth: { test: vi.fn().mockResolvedValue({ user_id: "bot-user" }) },
91+
auth: { test: vi.fn().mockResolvedValue({ user_id: "bot-user", bot_id: "bot-id" }) },
9092
conversations: {
9193
info: vi.fn().mockResolvedValue({
9294
channel: { name: "dm", is_im: true },
@@ -138,7 +140,12 @@ async function waitForSlackEvent(name: string) {
138140

139141
export function startSlackMonitor(
140142
monitorSlackProvider: SlackProviderMonitor,
141-
opts?: { botToken?: string; appToken?: string; channelRuntime?: ChannelRuntimeSurface },
143+
opts?: {
144+
botToken?: string;
145+
appToken?: string;
146+
channelRuntime?: ChannelRuntimeSurface;
147+
runtime?: RuntimeEnv;
148+
},
142149
) {
143150
const controller = new AbortController();
144151
const run = monitorSlackProvider({
@@ -147,6 +154,7 @@ export function startSlackMonitor(
147154
abortSignal: controller.signal,
148155
config: slackTestState.config,
149156
channelRuntime: opts?.channelRuntime,
157+
runtime: opts?.runtime,
150158
});
151159
return { controller, run };
152160
}
@@ -225,7 +233,7 @@ export function resetSlackTestState(config: Record<string, unknown> = defaultSla
225233
entries.map((input) => ({ input, resolved: false })),
226234
);
227235
const client = getSlackClient();
228-
client.auth.test.mockReset().mockResolvedValue({ user_id: "bot-user" });
236+
client.auth.test.mockReset().mockResolvedValue({ user_id: "bot-user", bot_id: "bot-id" });
229237
client.conversations.info.mockReset().mockResolvedValue({
230238
channel: { name: "dm", is_im: true },
231239
});

extensions/slack/src/monitor/provider.auth-test-token.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Slack tests cover auth.test token handling during provider boot.
2-
import { beforeEach, describe, expect, it } from "vitest";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
33
import {
44
getSlackClient,
55
resetSlackTestState,
@@ -28,4 +28,32 @@ describe("auth.test boot call", () => {
2828
expect(firstArg).not.toHaveProperty("token");
2929
}
3030
});
31+
32+
it("warns when auth.test returns a user id without bot_id", async () => {
33+
const runtimeLog = vi.fn();
34+
const client = getSlackClient();
35+
client.auth.test.mockResolvedValueOnce({
36+
user_id: "UUSER",
37+
user: "human-installer",
38+
team_id: "T1",
39+
team: "OpenClaw",
40+
});
41+
42+
const monitor = startSlackMonitor(monitorSlackProvider, {
43+
botToken: "xoxp-user-token",
44+
runtime: {
45+
log: runtimeLog,
46+
error: vi.fn(),
47+
exit: vi.fn(),
48+
},
49+
});
50+
await stopSlackMonitor(monitor);
51+
52+
expect(runtimeLog).toHaveBeenCalledWith(
53+
expect.stringContaining("channels.slack.accounts.default.botToken"),
54+
);
55+
expect(runtimeLog).toHaveBeenCalledWith(
56+
expect.stringContaining("replace it with a Bot User OAuth Token"),
57+
);
58+
});
3159
});

extensions/slack/src/monitor/provider.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ import { normalizeSlackWebhookPath, registerSlackHttpHandler } from "../http/ind
3838
import { SLACK_TEXT_LIMIT } from "../limits.js";
3939
import { resolveSlackChannelAllowlist } from "../resolve-channels.js";
4040
import { resolveSlackUserAllowlist, type SlackUserResolution } from "../resolve-users.js";
41-
import { resolveSlackAppToken, resolveSlackBotToken } from "../token.js";
41+
import {
42+
formatSlackBotTokenIdentityWarning,
43+
resolveSlackAppToken,
44+
resolveSlackBotToken,
45+
} from "../token.js";
4246
import { normalizeAllowList } from "./allow-list.js";
4347
import { resolveSlackSlashCommandConfig } from "./commands.js";
4448
import {
@@ -339,12 +343,17 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
339343
slackMode === "socket" ? parseApiAppIdFromAppToken(appToken) : undefined;
340344
let authTestFailed = false;
341345
let authTestError: string | undefined;
346+
let authIdentityWarning: string | undefined;
342347
try {
343348
const auth = await app.client.auth.test();
344349
botUserId = auth.user_id ?? "";
345350
botId = (auth as { bot_id?: string }).bot_id ?? "";
346351
teamId = auth.team_id ?? "";
347352
apiAppId = (auth as { api_app_id?: string }).api_app_id ?? "";
353+
authIdentityWarning = formatSlackBotTokenIdentityWarning({
354+
auth,
355+
accountId: account.accountId,
356+
});
348357
if (!botUserId) {
349358
authTestFailed = true;
350359
authTestError = "auth.test returned no user_id";
@@ -361,6 +370,9 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
361370
),
362371
);
363372
}
373+
if (authIdentityWarning) {
374+
runtime.log?.(warn(authIdentityWarning));
375+
}
364376

365377
if (apiAppId && expectedApiAppIdFromAppToken && apiAppId !== expectedApiAppIdFromAppToken) {
366378
runtime.error?.(

extensions/slack/src/probe.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ describe("probeSlack", () => {
4141
authTestMock.mockResolvedValue({
4242
ok: true,
4343
user_id: "U123",
44+
bot_id: "B123",
4445
user: "openclaw-bot",
4546
team_id: "T123",
4647
team: "OpenClaw",
@@ -60,6 +61,25 @@ describe("probeSlack", () => {
6061
expect(timeoutMs).toBe(2500);
6162
});
6263

64+
it("warns when auth.test looks like a user token in the bot token slot", async () => {
65+
vi.spyOn(Date, "now").mockReturnValueOnce(100).mockReturnValueOnce(145);
66+
authTestMock.mockResolvedValue({
67+
ok: true,
68+
user_id: "UUSER",
69+
user: "human-installer",
70+
team_id: "T123",
71+
team: "OpenClaw",
72+
});
73+
74+
await expect(probeSlack("xoxp-user-token", 2500, { accountId: "work" })).resolves.toMatchObject(
75+
{
76+
ok: true,
77+
warning:
78+
'Slack auth.test identified account "work" as user UUSER without bot_id. channels.slack.accounts.work.botToken appears to contain a user token; replace it with a Bot User OAuth Token. Until replaced, OpenClaw can mistake mentions of that user for bot mentions.',
79+
},
80+
);
81+
});
82+
6383
it("keeps optional auth metadata fields undefined when Slack omits them", async () => {
6484
vi.spyOn(Date, "now").mockReturnValueOnce(200).mockReturnValueOnce(235);
6585
authTestMock.mockResolvedValue({ ok: true });

extensions/slack/src/probe.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@ import type { BaseProbeResult } from "openclaw/plugin-sdk/channel-contract";
33
import { withTimeout } from "openclaw/plugin-sdk/text-utility-runtime";
44
import { createSlackWebClient } from "./client.js";
55
import { formatSlackError } from "./errors.js";
6+
import { formatSlackBotTokenIdentityWarning } from "./token.js";
67

78
export type SlackProbe = BaseProbeResult & {
89
status?: number | null;
910
elapsedMs?: number | null;
1011
bot?: { id?: string; name?: string };
1112
team?: { id?: string; name?: string };
13+
warning?: string;
1214
};
1315

14-
export async function probeSlack(token: string, timeoutMs = 2500): Promise<SlackProbe> {
16+
export async function probeSlack(
17+
token: string,
18+
timeoutMs = 2500,
19+
opts?: { accountId?: string | null },
20+
): Promise<SlackProbe> {
1521
const client = createSlackWebClient(token);
1622
const start = Date.now();
1723
try {
@@ -24,12 +30,17 @@ export async function probeSlack(token: string, timeoutMs = 2500): Promise<Slack
2430
elapsedMs: Date.now() - start,
2531
};
2632
}
33+
const warning = formatSlackBotTokenIdentityWarning({
34+
auth: result,
35+
accountId: opts?.accountId,
36+
});
2737
return {
2838
ok: true,
2939
status: 200,
3040
elapsedMs: Date.now() - start,
3141
bot: { id: result.user_id, name: result.user },
3242
team: { id: result.team_id, name: result.team },
43+
...(warning ? { warning } : {}),
3344
};
3445
} catch (err) {
3546
const message = formatSlackError(err);

0 commit comments

Comments
 (0)