Skip to content

Commit 8c915f0

Browse files
authored
feat(signal): add target aliases (#95738)
* feat(signal): add target aliases * fix(signal): canonicalize alias delivery targets * fix(signal): harden alias target resolution * test(signal): cover formatted media aliases * test(signal): align approval alias proof with request shape * Reuse Signal alias map for directory listing
1 parent 153fed7 commit 8c915f0

23 files changed

Lines changed: 1353 additions & 31 deletions

docs/channels/signal.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,58 @@ without explicit approvers; no-approver group approvals keep the local fallback
352352
- Groups: `signal:group:<groupId>`.
353353
- Usernames: `username:<name>` (if supported by your Signal account).
354354

355+
## Aliases
356+
357+
Configure aliases when you want stable names for recurring Signal targets.
358+
Aliases are OpenClaw-side config only; they do not create or edit Signal contacts.
359+
360+
```json5
361+
{
362+
channels: {
363+
signal: {
364+
aliases: {
365+
me: "+15557654321",
366+
jane: "uuid:123e4567-e89b-12d3-a456-426614174000",
367+
ops: "group:<groupId>",
368+
},
369+
defaultTo: "signal:me",
370+
},
371+
},
372+
}
373+
```
374+
375+
Use aliases anywhere Signal delivery targets are accepted:
376+
377+
```bash
378+
openclaw message send --channel signal --target signal:ops --message "Deployment is complete"
379+
```
380+
381+
Per-account aliases inherit the top-level aliases and can add or override names:
382+
383+
```json5
384+
{
385+
channels: {
386+
signal: {
387+
aliases: {
388+
me: "+15557654321",
389+
},
390+
accounts: {
391+
work: {
392+
aliases: {
393+
ops: "group:<workGroupId>",
394+
},
395+
},
396+
},
397+
},
398+
},
399+
}
400+
```
401+
402+
`openclaw directory peers list --channel signal` and
403+
`openclaw directory groups list --channel signal` list configured aliases. The
404+
Signal directory is config-backed; it does not live-query Signal contacts or
405+
mutate the Signal account.
406+
355407
## Troubleshooting
356408

357409
Run this ladder first:
@@ -416,10 +468,12 @@ Provider options:
416468
- `channels.signal.sendReadReceipts`: forward read receipts.
417469
- `channels.signal.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
418470
- `channels.signal.allowFrom`: DM allowlist (E.164 or `uuid:<id>`). `open` requires `"*"`. Signal has no usernames; use phone/UUID ids.
471+
- `channels.signal.aliases`: OpenClaw-side aliases for DM or group delivery targets.
419472
- `channels.signal.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
420473
- `channels.signal.groupAllowFrom`: group allowlist; accepts Signal group IDs (raw, `group:<id>`, or `signal:group:<id>`), sender E.164 numbers, or `uuid:<id>` values.
421474
- `channels.signal.groups`: per-group overrides keyed by Signal group id (or `"*"`). Supported fields: `requireMention`, `tools`, `toolsBySender`.
422475
- `channels.signal.accounts.<id>.groups`: per-account version of `channels.signal.groups` for multi-account setups.
476+
- `channels.signal.accounts.<id>.aliases`: per-account aliases, merged with top-level aliases.
423477
- `channels.signal.historyLimit`: max group messages to include as context (0 disables).
424478
- `channels.signal.dmHistoryLimit`: DM history limit in user turns. Per-user overrides: `channels.signal.dms["<phone_or_uuid>"].historyLimit`.
425479
- `channels.signal.textChunkLimit`: outbound chunk size (chars).

docs/cli/directory.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ openclaw message send --channel slack --target user:U012ABCDEF --message "hello"
3333
## ID formats (by channel)
3434

3535
- WhatsApp: `+15551234567` (DM), `[email protected]` (group), `120363123456789@newsletter` (Channel/Newsletter outbound target)
36+
- Signal: configured aliases resolve to E.164/UUID DM targets or `group:<id>` group targets
3637
- Telegram: `@username` or numeric chat id; groups are numeric ids
3738
- Slack: `user:U…` and `channel:C…`
3839
- Discord: `user:<id>` and `channel:<id>`

docs/docs_map.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
821821
- H2: Reactions (message tool)
822822
- H2: Approval reactions
823823
- H2: Delivery targets (CLI/cron)
824+
- H2: Aliases
824825
- H2: Troubleshooting
825826
- H2: Security notes
826827
- H2: Configuration reference (Signal)

extensions/signal/src/accounts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ function mergeSignalAccountConfig(cfg: OpenClawConfig, accountId: string): Signa
3232
| Record<string, Partial<SignalAccountConfig>>
3333
| undefined,
3434
accountId,
35+
nestedObjectKeys: ["aliases"],
3536
});
3637
}
3738

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
2+
// Signal tests cover alias target resolution behavior.
3+
import { describe, expect, it } from "vitest";
4+
import {
5+
listSignalAliasDirectoryEntries,
6+
resolveSignalAliasTarget,
7+
resolveSignalTarget,
8+
} from "./aliases.js";
9+
10+
describe("resolveSignalAliasTarget", () => {
11+
it("resolves top-level DM aliases to canonical targets", () => {
12+
const cfg = {
13+
channels: {
14+
signal: {
15+
aliases: {
16+
home: "+15551234567",
17+
},
18+
},
19+
},
20+
} as OpenClawConfig;
21+
22+
expect(resolveSignalAliasTarget({ cfg, input: "signal:home" })).toEqual({
23+
kind: "user",
24+
to: "+15551234567",
25+
alias: "home",
26+
});
27+
});
28+
29+
it("resolves account aliases after merging top-level aliases", () => {
30+
const cfg = {
31+
channels: {
32+
signal: {
33+
aliases: {
34+
home: "+15551234567",
35+
},
36+
accounts: {
37+
work: {
38+
aliases: {
39+
ops: "signal:group:VWATOdKF2hc8zdOS76q9tb0+5BI522e03QLDAq/9yPg=",
40+
},
41+
},
42+
},
43+
},
44+
},
45+
} as OpenClawConfig;
46+
47+
expect(resolveSignalAliasTarget({ cfg, accountId: "work", input: "ops" })).toEqual({
48+
kind: "group",
49+
to: "group:VWATOdKF2hc8zdOS76q9tb0+5BI522e03QLDAq/9yPg=",
50+
alias: "ops",
51+
});
52+
expect(resolveSignalAliasTarget({ cfg, accountId: "work", input: "home" })?.to).toBe(
53+
"+15551234567",
54+
);
55+
});
56+
57+
it("rejects recursive aliases before delivery", () => {
58+
const cfg = {
59+
channels: {
60+
signal: {
61+
aliases: {
62+
home: "signal:me",
63+
me: "home",
64+
},
65+
},
66+
},
67+
} as OpenClawConfig;
68+
69+
expect(() => resolveSignalAliasTarget({ cfg, input: "home" })).toThrow(
70+
'Signal alias "home" resolves recursively through "home".',
71+
);
72+
});
73+
74+
it("rejects aliases whose final value is not a Signal target", () => {
75+
const cfg = {
76+
channels: {
77+
signal: {
78+
aliases: {
79+
jane: "not a target",
80+
},
81+
},
82+
},
83+
} as OpenClawConfig;
84+
85+
expect(() => resolveSignalAliasTarget({ cfg, input: "jane" })).toThrow(
86+
'Signal alias "jane" must point to an E.164 number, uuid:<id>, username:<name>, or group:<id>.',
87+
);
88+
});
89+
90+
it("treats target-looking alias values as terminal targets before alias chaining", () => {
91+
const cfg = {
92+
channels: {
93+
signal: {
94+
aliases: {
95+
home: "+15551230000",
96+
"+15551230000": "+15559990000",
97+
},
98+
},
99+
},
100+
} as OpenClawConfig;
101+
102+
expect(resolveSignalAliasTarget({ cfg, input: "home" })).toEqual({
103+
kind: "user",
104+
to: "+15551230000",
105+
alias: "home",
106+
});
107+
});
108+
109+
it("resolves own prototype-shaped aliases without inheriting prototype keys", () => {
110+
const cfg = {
111+
channels: {
112+
signal: {
113+
aliases: {
114+
constructor: "+15551234567",
115+
toString: "group:VWATOdKF2hc8zdOS76q9tb0+5BI522e03QLDAq/9yPg=",
116+
},
117+
},
118+
},
119+
} as OpenClawConfig;
120+
121+
expect(resolveSignalAliasTarget({ cfg, input: "constructor" })).toEqual({
122+
kind: "user",
123+
to: "+15551234567",
124+
alias: "constructor",
125+
});
126+
expect(resolveSignalAliasTarget({ cfg, input: "toString" })).toEqual({
127+
kind: "group",
128+
to: "group:VWATOdKF2hc8zdOS76q9tb0+5BI522e03QLDAq/9yPg=",
129+
alias: "tostring",
130+
});
131+
132+
const ordinaryCfg = {
133+
channels: {
134+
signal: {
135+
aliases: {
136+
me: "+15551234567",
137+
},
138+
},
139+
},
140+
} as OpenClawConfig;
141+
142+
expect(resolveSignalAliasTarget({ cfg: ordinaryCfg, input: "constructor" })).toBeNull();
143+
});
144+
});
145+
146+
describe("resolveSignalTarget", () => {
147+
it("resolves aliases and raw targets through the same canonical parser", () => {
148+
const cfg = {
149+
channels: {
150+
signal: {
151+
aliases: {
152+
me: "uuid:123E4567-E89B-12D3-A456-426614174000",
153+
"+15551230000": "+15559990000",
154+
},
155+
},
156+
},
157+
} as OpenClawConfig;
158+
159+
expect(resolveSignalTarget({ cfg, input: "signal:me" })).toEqual({
160+
kind: "user",
161+
to: "123e4567-e89b-12d3-a456-426614174000",
162+
source: "alias",
163+
alias: "me",
164+
});
165+
expect(
166+
resolveSignalTarget({ cfg, input: "uuid:123E4567-E89B-12D3-A456-426614174000" }),
167+
).toEqual({
168+
kind: "user",
169+
to: "123e4567-e89b-12d3-a456-426614174000",
170+
source: "raw",
171+
});
172+
expect(resolveSignalTarget({ cfg, input: "+15551230000" })).toEqual({
173+
kind: "user",
174+
to: "+15551230000",
175+
source: "raw",
176+
});
177+
});
178+
});
179+
180+
describe("listSignalAliasDirectoryEntries", () => {
181+
it("lists alias-backed peers and groups with alias display names", () => {
182+
const cfg = {
183+
channels: {
184+
signal: {
185+
aliases: {
186+
me: "+15551234567",
187+
ops: "group:VWATOdKF2hc8zdOS76q9tb0+5BI522e03QLDAq/9yPg=",
188+
},
189+
},
190+
},
191+
} as OpenClawConfig;
192+
193+
expect(listSignalAliasDirectoryEntries({ cfg, kind: "user" })).toEqual([
194+
{ kind: "user", id: "+15551234567", name: "me" },
195+
]);
196+
expect(listSignalAliasDirectoryEntries({ cfg, kind: "user", query: "1555" })).toEqual([
197+
{ kind: "user", id: "+15551234567", name: "me" },
198+
]);
199+
expect(listSignalAliasDirectoryEntries({ cfg, kind: "group" })).toEqual([
200+
{
201+
kind: "group",
202+
id: "group:VWATOdKF2hc8zdOS76q9tb0+5BI522e03QLDAq/9yPg=",
203+
name: "ops",
204+
},
205+
]);
206+
});
207+
208+
it("lists aliases that resolve through another alias", () => {
209+
const cfg = {
210+
channels: {
211+
signal: {
212+
aliases: {
213+
me: "+15551234567",
214+
home: "signal:me",
215+
},
216+
},
217+
},
218+
} as OpenClawConfig;
219+
220+
expect(listSignalAliasDirectoryEntries({ cfg, kind: "user" })).toEqual([
221+
{ kind: "user", id: "+15551234567", name: "me" },
222+
{ kind: "user", id: "+15551234567", name: "home" },
223+
]);
224+
});
225+
226+
it("does not let fuzzy peer matches shadow exact group aliases", () => {
227+
const cfg = {
228+
channels: {
229+
signal: {
230+
aliases: {
231+
ops: "group:VWATOdKF2hc8zdOS76q9tb0+5BI522e03QLDAq/9yPg=",
232+
"ops-dm": "+15551234567",
233+
},
234+
},
235+
},
236+
} as OpenClawConfig;
237+
238+
expect(listSignalAliasDirectoryEntries({ cfg, kind: "user", query: "ops" })).toEqual([]);
239+
expect(listSignalAliasDirectoryEntries({ cfg, kind: "group", query: "ops" })).toEqual([
240+
{
241+
kind: "group",
242+
id: "group:VWATOdKF2hc8zdOS76q9tb0+5BI522e03QLDAq/9yPg=",
243+
name: "ops",
244+
},
245+
]);
246+
});
247+
248+
it("fails invalid exact aliases instead of falling through to fuzzy matches", () => {
249+
const cfg = {
250+
channels: {
251+
signal: {
252+
aliases: {
253+
ops: "not-a-signal-target",
254+
"ops-dm": "+15551234567",
255+
},
256+
},
257+
},
258+
} as OpenClawConfig;
259+
260+
expect(() => listSignalAliasDirectoryEntries({ cfg, kind: "user", query: "ops" })).toThrow(
261+
'Signal alias "ops" must point to an E.164 number, uuid:<id>, username:<name>, or group:<id>.',
262+
);
263+
});
264+
});

0 commit comments

Comments
 (0)