Skip to content

Commit 35e1605

Browse files
committed
feat: add configurable context visibility
1 parent d4d2d9e commit 35e1605

31 files changed

Lines changed: 406 additions & 2 deletions

SECURITY.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ These are frequently reported but are typically closed with no code change:
5656
- Authorized user-triggered local actions presented as privilege escalation. Example: an allowlisted/owner sender running `/export-session /absolute/path.html` to write on the host. In this trust model, authorized user actions are trusted host actions unless you demonstrate an auth/sandbox/boundary bypass.
5757
- Reports that only show a malicious plugin executing privileged actions after a trusted operator installs/enables it.
5858
- Reports that assume per-user multi-tenant authorization on a shared gateway host/config.
59+
- Reports that only show quoted/replied/thread/forwarded supplemental context from non-allowlisted senders being visible to the model, without demonstrating an auth, policy, approval, or sandbox boundary bypass.
5960
- Reports that treat the Gateway HTTP compatibility endpoints (`POST /v1/chat/completions`, `POST /v1/responses`) as if they implemented scoped operator auth (`operator.write` vs `operator.admin`). These endpoints authenticate the shared Gateway bearer secret/password and are documented full operator-access surfaces, not per-user/per-scope boundaries.
6061
- Reports that assume `x-openclaw-scopes` can reduce or redefine shared-secret bearer auth on the OpenAI-compatible HTTP endpoints. For shared-secret auth (`gateway.auth.mode="token"` or `"password"`), those endpoints ignore narrower bearer-declared scopes and restore the full default operator scope set plus owner semantics.
6162
- Reports that treat `POST /tools/invoke` under shared-secret bearer auth (`gateway.auth.mode="token"` or `"password"`) as a narrower per-request/per-scope authorization surface. That endpoint is designed as the same trusted-operator HTTP boundary: shared-secret bearer auth is full operator access there, narrower `x-openclaw-scopes` values do not reduce that path, and owner-only tool policy follows the shared-secret operator contract.
@@ -167,6 +168,24 @@ OpenClaw's security model is "personal assistant" (one trusted operator, potenti
167168
- For company-shared setups, use a dedicated machine/VM/container and dedicated accounts; avoid mixing personal data on that runtime.
168169
- If that host/browser profile is logged into personal accounts (for example Apple/Google/personal password manager), you have collapsed the boundary and increased personal-data exposure risk.
169170

171+
## Context Visibility and Allowlists
172+
173+
OpenClaw distinguishes:
174+
175+
- **Trigger authorization**: who can trigger the agent (`dmPolicy`, `groupPolicy`, allowlists, mention gates)
176+
- **Context visibility**: what supplemental context is provided to the model (reply body, quoted text, thread history, forwarded metadata)
177+
178+
In current releases, allowlists primarily gate triggering and owner-style command access. They do not guarantee universal supplemental-context redaction across every channel/surface.
179+
180+
Current channel behavior is not fully uniform:
181+
182+
- some channels already filter parts of supplemental context by sender allowlist
183+
- other channels still pass supplemental context as received
184+
185+
Reports that only show supplemental-context visibility differences are typically hardening/consistency findings unless they also demonstrate a documented boundary bypass (auth, policy, approvals, sandbox, or equivalent).
186+
187+
Hardening roadmap may add explicit visibility modes (for example `all`, `allowlist`, `allowlist_quote`) so operators can opt into stricter context filtering with predictable tradeoffs.
188+
170189
## Agent and Model Assumptions
171190

172191
- The model/agent is **not** a trusted principal. Assume prompt/content injection can manipulate behavior.

docs/channels/discord.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,8 @@ Default slash command settings:
643643
- thread config inherits parent channel config unless a thread-specific entry exists
644644

645645
Channel topics are injected as **untrusted** context (not as system prompt).
646+
Reply and quoted-message context currently stays as received.
647+
Discord allowlists primarily gate who can trigger the agent, not a full supplemental-context redaction boundary.
646648

647649
</Accordion>
648650

docs/channels/groups.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,28 @@ requireMention? yes -> mentioned? no -> store for context only
3636
otherwise -> reply
3737
```
3838

39+
## Context visibility and allowlists
40+
41+
Two different controls are involved in group safety:
42+
43+
- **Trigger authorization**: who can trigger the agent (`groupPolicy`, `groups`, `groupAllowFrom`, channel-specific allowlists).
44+
- **Context visibility**: what supplemental context is injected into the model (reply text, quotes, thread history, forwarded metadata).
45+
46+
By default, OpenClaw prioritizes normal chat behavior and keeps context mostly as received. This means allowlists primarily decide who can trigger actions, not a universal redaction boundary for every quoted or historical snippet.
47+
48+
Current behavior is channel-specific:
49+
50+
- Some channels already apply sender-based filtering for supplemental context in specific paths (for example Slack thread seeding, Matrix reply/thread lookups).
51+
- Other channels still pass quote/reply/forward context through as received.
52+
53+
Hardening direction (planned):
54+
55+
- `contextVisibility: "all"` (default) keeps current as-received behavior.
56+
- `contextVisibility: "allowlist"` filters supplemental context to allowlisted senders.
57+
- `contextVisibility: "allowlist_quote"` is `allowlist` plus one explicit quote/reply exception.
58+
59+
Until this hardening model is implemented consistently across channels, expect differences by surface.
60+
3961
![Group message flow](/images/groups-flow.svg)
4062

4163
If you want...

docs/channels/matrix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,7 @@ Current behavior:
590590
- The current trigger message is not included in `InboundHistory`; it stays in the main inbound body for that turn.
591591
- Retries of the same Matrix event reuse the original history snapshot instead of drifting forward to newer room messages.
592592
- Fetched room context (including reply and thread context lookups) is filtered by sender allowlists (`groupAllowFrom`), so non-allowlisted messages are excluded from agent context.
593+
- This filtering is channel-level hardening behavior. Other channels may still expose supplemental context as received.
593594

594595
## DM and room policy example
595596

docs/channels/msteams.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,8 @@ The action is gated by `channels.msteams.actions.memberInfo` (default: enabled w
302302
- `channels.msteams.historyLimit` controls how many recent channel/group messages are wrapped into the prompt.
303303
- Falls back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
304304
- Fetched thread history is filtered by sender allowlists (`allowFrom` / `groupAllowFrom`), so thread context seeding only includes messages from allowed senders.
305+
- Quoted attachment context (`ReplyTo*` derived from Teams reply HTML) is currently passed as received.
306+
- In other words, allowlists gate who can trigger the agent; only specific supplemental context paths are filtered today.
305307
- DM history can be limited with `channels.msteams.dmHistoryLimit` (user turns). Per-user overrides: `channels.msteams.dms["<user_id>"].historyLimit`.
306308

307309
## Current Teams RSC Permissions (Manifest)

docs/channels/slack.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ Current Slack message actions include `send`, `upload-file`, `download-file`, `r
354354
- Assistant thread status updates (for "is typing..." indicators in threads) use `assistant.threads.setStatus` and require bot scope `assistant:write`.
355355
- `channel_id_changed` can migrate channel config keys when `configWrites` is enabled.
356356
- Channel topic/purpose metadata is treated as untrusted context and can be injected into routing context.
357+
- Thread starter and initial thread-history context seeding are filtered by configured sender allowlists when applicable.
357358
- Block actions and modal interactions emit structured `Slack interaction: ...` system events with rich payload fields:
358359
- block actions: selected values, labels, picker values, and `workflow_*` metadata
359360
- modal `view_submission` and `view_closed` events with routed channel metadata and form inputs

docs/channels/telegram.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,8 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
759759
- `channels.telegram.mediaMaxMb` (default 100) caps inbound and outbound Telegram media size.
760760
- `channels.telegram.timeoutSeconds` overrides Telegram API client timeout (if unset, grammY default applies).
761761
- group context history uses `channels.telegram.historyLimit` or `messages.groupChat.historyLimit` (default 50); `0` disables.
762+
- reply/quote/forward supplemental context is currently passed as received.
763+
- Telegram allowlists primarily gate who can trigger the agent, not a full supplemental-context redaction boundary.
762764
- DM history controls:
763765
- `channels.telegram.dmHistoryLimit`
764766
- `channels.telegram.dms["<user_id>"].historyLimit`

docs/gateway/security/index.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,29 @@ If more than one person can DM your bot:
169169
- Never combine shared DMs with broad tool access.
170170
- This hardens cooperative/shared inboxes, but is not designed as hostile co-tenant isolation when users share host/config write access.
171171

172+
## Context visibility model
173+
174+
OpenClaw separates two concepts:
175+
176+
- **Trigger authorization**: who can trigger the agent (`dmPolicy`, `groupPolicy`, allowlists, mention gates).
177+
- **Context visibility**: what supplemental context is injected into model input (reply body, quoted text, thread history, forwarded metadata).
178+
179+
In the current product, allowlists primarily gate triggers and command authorization. They are not a guaranteed universal redaction boundary for every supplemental context field on every channel.
180+
181+
Current behavior is channel-specific:
182+
183+
- Some channels already filter parts of supplemental context by sender allowlists.
184+
- Other channels still pass supplemental context through as received.
185+
186+
Advisory triage guidance:
187+
188+
- Claims that only show "model can see quoted or historical text from non-allowlisted senders" are usually hardening and consistency findings, not auth or sandbox boundary bypasses by themselves.
189+
- To be security-impacting, reports still need a demonstrated trust-boundary bypass (auth, policy, sandbox, approval, or another documented boundary).
190+
191+
Hardening direction:
192+
193+
- OpenClaw maintainers may introduce explicit context visibility modes such as `all`, `allowlist`, and `allowlist_quote` to make this behavior intentional and configurable across channels.
194+
172195
## What the audit checks (high level)
173196

174197
- **Inbound access** (DM policies, group policies, allowlists): can strangers trigger the bot?
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
resolveChannelContextVisibilityMode,
4+
resolveDefaultContextVisibility,
5+
} from "./context-visibility.js";
6+
7+
describe("resolveDefaultContextVisibility", () => {
8+
it("reads channels.defaults.contextVisibility", () => {
9+
expect(
10+
resolveDefaultContextVisibility({
11+
channels: {
12+
defaults: {
13+
contextVisibility: "allowlist_quote",
14+
},
15+
},
16+
}),
17+
).toBe("allowlist_quote");
18+
});
19+
});
20+
21+
describe("resolveChannelContextVisibilityMode", () => {
22+
it("prefers explicitly provided mode", () => {
23+
expect(
24+
resolveChannelContextVisibilityMode({
25+
cfg: {},
26+
channel: "slack",
27+
configuredContextVisibility: "allowlist",
28+
}),
29+
).toBe("allowlist");
30+
});
31+
32+
it("falls back to account mode then channel mode then defaults", () => {
33+
const cfg = {
34+
channels: {
35+
defaults: {
36+
contextVisibility: "allowlist_quote",
37+
},
38+
slack: {
39+
contextVisibility: "allowlist",
40+
accounts: {
41+
work: {
42+
contextVisibility: "all",
43+
},
44+
},
45+
},
46+
},
47+
};
48+
expect(
49+
resolveChannelContextVisibilityMode({
50+
cfg,
51+
channel: "slack",
52+
accountId: "work",
53+
}),
54+
).toBe("all");
55+
expect(
56+
resolveChannelContextVisibilityMode({
57+
cfg,
58+
channel: "slack",
59+
accountId: "missing",
60+
}),
61+
).toBe("allowlist");
62+
expect(
63+
resolveChannelContextVisibilityMode({
64+
cfg: { channels: { defaults: { contextVisibility: "allowlist_quote" } } },
65+
channel: "signal",
66+
}),
67+
).toBe("allowlist_quote");
68+
});
69+
70+
it("defaults to all when unset", () => {
71+
expect(
72+
resolveChannelContextVisibilityMode({
73+
cfg: {},
74+
channel: "telegram",
75+
}),
76+
).toBe("all");
77+
});
78+
});

src/config/context-visibility.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { resolveAccountEntry } from "../routing/account-lookup.js";
2+
import { normalizeAccountId } from "../routing/session-key.js";
3+
import type { OpenClawConfig } from "./config.js";
4+
import type { ContextVisibilityMode } from "./types.base.js";
5+
6+
type ChannelContextVisibilityConfig = {
7+
contextVisibility?: ContextVisibilityMode;
8+
accounts?: Record<string, { contextVisibility?: ContextVisibilityMode }>;
9+
};
10+
11+
export type ContextVisibilityDefaultsConfig = {
12+
channels?: {
13+
defaults?: {
14+
contextVisibility?: ContextVisibilityMode;
15+
};
16+
};
17+
};
18+
19+
export function resolveDefaultContextVisibility(
20+
cfg: ContextVisibilityDefaultsConfig,
21+
): ContextVisibilityMode | undefined {
22+
return cfg.channels?.defaults?.contextVisibility;
23+
}
24+
25+
export function resolveChannelContextVisibilityMode(params: {
26+
cfg: OpenClawConfig;
27+
channel: string;
28+
accountId?: string | null;
29+
configuredContextVisibility?: ContextVisibilityMode;
30+
}): ContextVisibilityMode {
31+
if (params.configuredContextVisibility) {
32+
return params.configuredContextVisibility;
33+
}
34+
const channelConfig = params.cfg.channels?.[params.channel] as
35+
| ChannelContextVisibilityConfig
36+
| undefined;
37+
const accountId = normalizeAccountId(params.accountId);
38+
const accountMode = resolveAccountEntry(channelConfig?.accounts, accountId)?.contextVisibility;
39+
return (
40+
accountMode ??
41+
channelConfig?.contextVisibility ??
42+
resolveDefaultContextVisibility(params.cfg) ??
43+
"all"
44+
);
45+
}

0 commit comments

Comments
 (0)