fix(line): require wildcard for open dm policy

This commit is contained in:
Peter Steinberger
2026-05-06 07:35:10 +01:00
parent 9d3dcfdd51
commit 430c0bdaba
4 changed files with 98 additions and 42 deletions

View File

@@ -68,6 +68,22 @@ Minimal config:
}
```
Public DM config:
```json5
{
channels: {
line: {
enabled: true,
channelAccessToken: "LINE_CHANNEL_ACCESS_TOKEN",
channelSecret: "LINE_CHANNEL_SECRET",
dmPolicy: "open",
allowFrom: ["*"],
},
},
}
```
Env vars (default account only):
- `LINE_CHANNEL_ACCESS_TOKEN`
@@ -119,7 +135,7 @@ openclaw pairing approve line <CODE>
Allowlists and policies:
- `channels.line.dmPolicy`: `pairing | allowlist | open | disabled`
- `channels.line.allowFrom`: allowlisted LINE user IDs for DMs
- `channels.line.allowFrom`: allowlisted LINE user IDs for DMs; `dmPolicy: "open"` requires `["*"]`
- `channels.line.groupPolicy`: `allowlist | open | disabled`
- `channels.line.groupAllowFrom`: allowlisted LINE user IDs for groups
- Per-group overrides: `channels.line.groups.<groupId>.allowFrom`

View File

@@ -0,0 +1,51 @@
import { describe, expect, it } from "vitest";
import { LineConfigSchema } from "./config-schema.js";
describe("LineConfigSchema", () => {
it('rejects dmPolicy="open" without wildcard allowFrom', () => {
const result = LineConfigSchema.safeParse({
channelAccessToken: "token",
channelSecret: "secret",
dmPolicy: "open",
});
expect(result.success).toBe(false);
expect(result.error.issues).toEqual([
expect.objectContaining({
path: ["allowFrom"],
message: 'channels.line.dmPolicy="open" requires channels.line.allowFrom to include "*"',
}),
]);
});
it('accepts dmPolicy="open" with wildcard allowFrom', () => {
const result = LineConfigSchema.safeParse({
channelAccessToken: "token",
channelSecret: "secret",
dmPolicy: "open",
allowFrom: ["*"],
});
expect(result.success).toBe(true);
});
it('rejects account dmPolicy="open" without wildcard allowFrom', () => {
const result = LineConfigSchema.safeParse({
accounts: {
work: {
channelAccessToken: "token",
channelSecret: "secret",
dmPolicy: "open",
},
},
});
expect(result.success).toBe(false);
expect(result.error.issues).toEqual([
expect.objectContaining({
path: ["accounts", "work", "allowFrom"],
message: 'channels.line.dmPolicy="open" requires channels.line.allowFrom to include "*"',
}),
]);
});
});

View File

@@ -1,4 +1,8 @@
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
import {
buildChannelConfigSchema,
requireOpenAllowFrom,
} from "openclaw/plugin-sdk/channel-config-schema";
import { requireChannelOpenAllowFrom } from "openclaw/plugin-sdk/extension-shared";
import { z } from "openclaw/plugin-sdk/zod";
const DmPolicySchema = z.enum(["open", "allowlist", "pairing", "disabled"]);
@@ -15,7 +19,7 @@ const ThreadBindingsSchema = z
})
.strict();
const LineCommonConfigSchema = z.object({
const LineCommonConfigSchemaBase = z.object({
enabled: z.boolean().optional(),
channelAccessToken: z.string().optional(),
channelSecret: z.string().optional(),
@@ -42,15 +46,35 @@ const LineGroupConfigSchema = z
})
.strict();
const LineAccountConfigSchema = LineCommonConfigSchema.extend({
const LineAccountConfigSchema = LineCommonConfigSchemaBase.extend({
groups: z.record(z.string(), LineGroupConfigSchema.optional()).optional(),
}).strict();
})
.strict()
.superRefine((value, ctx) => {
requireChannelOpenAllowFrom({
channel: "line",
policy: value.dmPolicy,
allowFrom: value.allowFrom,
ctx,
requireOpenAllowFrom,
});
});
export const LineConfigSchema = LineCommonConfigSchema.extend({
export const LineConfigSchema = LineCommonConfigSchemaBase.extend({
accounts: z.record(z.string(), LineAccountConfigSchema.optional()).optional(),
defaultAccount: z.string().optional(),
groups: z.record(z.string(), LineGroupConfigSchema.optional()).optional(),
}).strict();
})
.strict()
.superRefine((value, ctx) => {
requireChannelOpenAllowFrom({
channel: "line",
policy: value.dmPolicy,
allowFrom: value.allowFrom,
ctx,
requireOpenAllowFrom,
});
});
export const LineChannelConfigSchema = buildChannelConfigSchema(LineConfigSchema);

View File

@@ -2907,41 +2907,6 @@ describe("dispatchTelegramMessage draft streaming", () => {
expect(statusReactionController.setTool).toHaveBeenCalledWith("exec");
});
it("keeps non-command Telegram progress draft lines across post-tool assistant boundaries", async () => {
const draftStream = createSequencedDraftStream(2001);
createTelegramDraftStream.mockReturnValue(draftStream);
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
async ({ dispatcherOptions, replyOptions }) => {
await replyOptions?.onReplyStart?.();
await replyOptions?.onAssistantMessageStart?.();
await replyOptions?.onItemEvent?.({ kind: "search", progressText: "docs lookup" });
await replyOptions?.onItemEvent?.({ progressText: "tests passed" });
await replyOptions?.onAssistantMessageStart?.();
await dispatcherOptions.deliver({ text: "Final after tool" }, { kind: "final" });
return { queuedFinal: true };
},
);
await dispatchWithContext({
context: createContext(),
streamMode: "progress",
telegramCfg: { streaming: { mode: "progress", progress: { label: "Shelling" } } },
});
expect(draftStream.update).toHaveBeenCalledWith(
expect.stringMatching(/^Shelling\n`🔎 Web Search: docs lookup`\n• `tests passed`$/),
);
expect(draftStream.forceNewMessage).not.toHaveBeenCalled();
expect(draftStream.materialize).not.toHaveBeenCalled();
expect(draftStream.clear).toHaveBeenCalledTimes(1);
expect(deliverReplies).toHaveBeenCalledWith(
expect.objectContaining({
replies: [expect.objectContaining({ text: "Final after tool" })],
}),
);
expect(editMessageTelegram).not.toHaveBeenCalled();
});
it("keeps DM reasoning block updates in preview flow without sending duplicates", async () => {
const answerDraftStream = createDraftStream(999);
let previewRevision = 0;