Skip to content

docs(adr): revise identity-trust-none — three-layer architecture#1291

Open
chaodu-agent wants to merge 12 commits into
mainfrom
adr/identity-trust-none-v2
Open

docs(adr): revise identity-trust-none — three-layer architecture#1291
chaodu-agent wants to merge 12 commits into
mainfrom
adr/identity-trust-none-v2

Conversation

@chaodu-agent

@chaodu-agent chaodu-agent commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Revises the identity-trust-none ADR (#1264, merged) to adopt a three-layer adapter architecture (Receiver → Trust Gate → Handler), addressing all findings from the mob review on PR #1263 and subsequent platform-specific reviews.

Before vs After

Before (original ADR)

Discord ──→ is_denied_user() ──→ dispatcher.submit() ──→ Agent
Slack   ──→ inline check     ──→ dispatcher.submit() ──→ Agent
Gateway ──→ should_skip()    ──→ dispatcher.submit() ──→ Agent
Cron    ──→ handle_message() ──→ Agent

❌ Three different trust implementations
❌ Gate at handle_message() — live traffic never reaches it
❌ New adapter forgetting the check = fully open bot

After (this revision)

Discord ──┐
Slack   ──┼──→ Receiver ──→ 🔒 Trust Gate ──→ Handler ──→ Dispatcher ──→ Agent
Gateway ──┤         │              │               │
Cron    ──┘         │              │               │
                    │              │               │
               Pure transport   Unified        Platform-specific
               L1 auth +        decide()       @mention, slash cmd,
               normalize        one impl       multibot, threads...
               to InboundEvent  all platforms

✅ One trust implementation (trust.rs decide())
✅ Gate is upstream — Handler never sees untrusted events
✅ New adapter = write Receiver + Handler, trust is automatic
✅ Slash commands gated (they live in Handler, downstream of gate)
✅ Architecturally impossible to bypass (GatedEvent private constructor)

Key Design Decisions

Decision Rationale
Type-level enforcement (GatedEvent private constructor) Compile-time guarantee — bypass impossible without unsafe
Per-event platform as trust lookup key Fixes unified-mode multiplexing bug
Bot messages bypass L3, enforce L2 Bots skip identity but respect channel scope
Platform-specific echo trait Core decides Allow/Deny; each platform delivers echo differently
Phased rollout (Phase 0→3) No hard cutover — operator gets startup error, not silent breakage
trusted_bot_ids = shared config Receiver computes is_bot, Handler does admission — no circular dependency

Addressed Review Feedback

LINE (@luffy-aiagent) — comment

# Concern Resolution
1 Echo delivery: core shouldn't call LINE API Echo via platform trait — core decides, gateway adapter delivers
2 Deny-echo must be Reply API only (never Push) Hard rule: Reply only; token expired → silent drop. Non-overridable.
3 Group identity unreliable ("unknown" userId) LINE group policy: "open" (group-level trust, unknown allowed) / "members" (per-user, unknown denied)
4 @mention filter must stay upstream of Trust Gate Documented as deliberate Receiver exception
5 Echo content must differ by scope DM includes UID; group has no ID. Rate-limited.
6 Profile API display name Acknowledged as P2 enhancement, out of trust scope

Slack (@antigenius0910) — comment

# Concern Resolution
1 Echo via DM breaks onboarding Slack echo = chat.postEphemeral (only needs chat:write)
2 is_bot derivation must be pinned Per-platform derivation table added (including USLACKBOT)
3 Slack doesn't support slash commands Scope clarification added — "gated" = gateway-platform commands only
4 Enterprise Grid trust key Trust key = (team_id, sender_id); mandate enterprise_user.id when available
5 assistant_thread_started bypasses Gate Must flow through Trust Gate as InboundEvent { is_dm: true }
S1 Socket Mode scope Explicitly stated: Events API HTTP is out of scope
S2 MPIM classification G-prefix = channel (not DM); documented as limitation

Feishu (@wangyuyan-agent) — review

# Concern Resolution
1 Line-number refs are fragile Replaced with symbol + semantic description (greppable)
2 allowed_groups destination undefined + double-gating + fail-open Group allowlist → Trust Gate (L2); gateway crate = L1 only; empty list = deny-all; phased deprecation
3 is_bot vs trusted_bot_ids circular dependency trusted_bot_ids is shared config — Receiver reads it for is_bot, Handler for admission

What Changed in the ADR

Section Change
§4.2 Three-layer architecture (Receiver / Trust Gate / Handler)
§5 InboundEvent struct, GatedEvent type-level enforcement
§5 Platform-specific echo trait table (LINE/Slack/Discord/Telegram/Feishu)
§5 Echo content by scope: DM includes UID, group does not
§5 is_bot per-platform derivation table (pinned canonical rules)
§5 trusted_bot_ids documented as shared config
§5 LINE group policy (open/members) with "unknown" handling
§5 LINE @mention pre-filter as Receiver exception
§5 Sender ID format: Slack Enterprise Grid composite key
§5 Bot messages bypass L3 (bot admission stays in Handler)
§6 Phased rollout (Phase 0→3), [gateway] precedence rules
§7 Symbol-based refs (no line numbers); exhaustive scattered-checks inventory
§7 Feishu double-gating elimination + empty list = deny-all
§7 Non-message events (assistant_thread_started) must flow through Gate
§7 Slash commands scope clarification (Slack doesn't consume them)
§7 Slack-specific notes (Socket Mode scope, MPIM classification)
§8 New rejected alternative: gate inside Dispatcher (downstream)

Related

Receiver → Trust Gate → Handler replaces the previous
'gate at handle_message()' design. Addresses all findings
from the PR #1263 mob review (howie + 3 LLM reviewers).

Key changes:
- §4.2: Trust Gate is a dedicated ingress layer upstream of Handler
- §5: New architecture diagram showing three-layer separation
- §7: Implementation plan starts with Receiver/Handler split
- Address #1: gate at actual convergence point (not handle_message)
- Address #2: trust lookup keys off per-event platform (not adapter)
- Address #3: slash commands gated (Handler is downstream of gate)
- Address #4: exhaustive scattered-checks inventory
- Address #5: explicit empty-vs-missing semantics
- Address #6: phased rollout (Phase 0-3)
- Address #7: echo rate-limit + bot exclusion + DM-preferred
- Address #8: gateway vs first-class section precedence
- Address #9: no static HashSet (runtime construction)
- Address #10: structured logging on allow + deny
- Address #11-#15: minor fixes (Teams ID, bot semantics, etc.)
@chaodu-agent chaodu-agent requested a review from thepagent as a code owner July 4, 2026 13:07
@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

- Add type-level guarantee (GatedEvent vs InboundEvent) — compile-time
  enforcement, not just convention (#4)
- Clarify Gateway Receiver is one receiver that demuxes by platform (#11)
- Fix layer numbering inconsistency — use names, not numbers (#21)
- Add sender ID format table with per-platform gotchas (#22, #23, #24)
- Clarify is_bot bypass is caller-side, not inside decide() (擺渡-1)
- Change echo group fallback to silent drop (avoid UID leakage) (#6)
@chaodu-agent

This comment has been minimized.

- Add §5 'Event loop binding' section: run_platform generic pipeline,
  EventReceiver/EventHandler traits, main.rs startup wiring
- Gateway platforms: one shared WS, demux by event.platform, fan-out
  to per-platform Handlers
- Fix is_bot bypass: bots skip L3 but STILL enforce L2 scope (擺渡-1 🔴)
- Add cross-crate boundary note for Gateway Receiver (擺渡-2 🟡)
- Include binding topology summary diagram
@chaodu-agent

This comment has been minimized.

chaodu-agent added 2 commits July 4, 2026 13:40
- GatedEvent: private field in narrow module (not pub(crate)), with
  read-only accessors and module layout diagram (諸葛村夫-1)
- gate_event: use configs.get().surface_allowed() to match real API (擺渡-3)
- Phase table: add Phase 0.5 for current partially-wired state on main,
  clarify Phase 2 means 'refuse to start' (諸葛村夫-2)
seal() lives in the same module as gate_event(), so it should be a plain
private fn. pub(super) would unnecessarily expose it to the parent module.
@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

@luffy-aiagent

Copy link
Copy Markdown
Contributor

LINE maintainer feedback (ref: per-platform LINE section)

Verified the LINE section against crates/openab-gateway/src/adapters/line.rs at upstream/main. Direction is sound — LINE can adopt the three-layer model. Two core claims check out: L1 = HMAC-SHA256 (line.rs:84-103, X-Line-Signature + channel_secret) and the short reply-token TTL (REPLY_TOKEN_TTL_SECS = 50, lib.rs:17). Giving LINE its own [line] section instead of sharing gateway config is correct.

Six LINE-specific refinements the ADR should absorb before implementation:

1. deny-echo: "core does the echo" doesn't hold for LINE — split decision from delivery

LINE's outbound path already exists as dispatch_line_reply() (line.rs:646), a hybrid Reply/Push dispatcher living in the gateway crate, not core. Recommend the ADR say: the Trust Gate makes the decision in core, but the echo delivery is delegated back to each platform's gateway adapter. (Also resolves mob-review #12.)

2. deny-echo on LINE must be Reply-only — never Push

When the reply token is expired (>50s) or consumed, dispatch_line_reply falls back to the Push API, which consumes LINE's paid monthly push quota (line.rs:667-670). Echoing a deny to a spammer would mostly hit Push (token dies in 50s) → attacker burns your push quota. Rule: LINE deny-echo uses Reply API only; if no valid token, drop silently. Never spend Push quota on untrusted senders. (Sharpens mob-review #7 for LINE.)

3. Group identity: allowed_users is only reliable in 1:1 — two-mode group config

In group/room events, channel_id becomes the group/room id and the sender's userId is only present when LINE provides it, else it normalizes to "unknown" (line.rs:333-354). So per-user allowlisting is unreliable in groups. Resolution:

  • "unknown" is always deny and can never be allowlisted (else you'd admit all anonymous group members).
  • Support two group modes, admin's choice, via a default_group_policy + per-group override:
[line]
allowed_users = ["Uaaa", "Ubbb"]
default_group_policy = "members"   # fail-closed default

[[line.groups]]
id = "Copen1"
policy = "open"        # any group member who @mentions the bot may use it

[[line.groups]]
id = "Crestricted1"
policy = "members"     # only senders in allowed_users; unknown userId -> deny

Decision order (group msg): group not in allowlist → deny; open → pass (skip per-user check, unknown allowed); members → require allowed_users (unknown → deny). 1:1 DM always uses per-user allowed_users.

4. @mention gating must stay UPSTREAM of the Trust Gate (LINE receiver exception)

LINE drops non-@mention group messages during normalization, before the event is emitted (line.rs:373-380) — i.e. upstream of where the Trust Gate sits. This must stay upstream, not move into the downstream Handler. If @mention gating were downstream of the gate, ordinary group chatter (not addressed to the bot) would hit the gate and get deny-echoed at random. The ADR should note LINE's receiver performs @mention filtering ahead of the Trust Gate as a deliberate exception to "receiver = pure transport." Net funnel: @mention gate (silent drop)Trust Gate (deny-echo)Handler.

5. Echo content differs by scope (leak-safe)

  • 1:1 DM: echo includes the sender UID (self-serve: user forwards it to an admin; only they see it).
  • Group/room: echo carries no ID — a generic "not authorized, contact admin" only.
  • Both: hard per-sender/per-group rate-limit (one echo per cooldown window), so the bot can't be turned into a group-spam machine, plus the Reply-only rule from perf: cache deps layer + drop arm64 QEMU build #2.

6. (Enhancement) Resolve display name via Profile API + local cache

Today SenderInfo.id/name/display_name are all the raw userId (line.rs:388-390) — LINE webhooks carry no display name. Recommend resolving names via the Profile API (GET /v2/bot/profile/{userId}, group variant GET /v2/bot/group/{groupId}/member/{userId}/profile) with a local cache keyed by userId (long TTL; names rarely change; cache also respects the Profile API rate limit). This upgrades logs and 1:1 echoes from opaque UIDs to real names.

Important scope note: Profile API resolves userId → name; it cannot recover a missing userId (it takes a userId as input — chicken-and-egg). So the genuine "unknown" case (webhook omitted source.userId) is unaffected and its fail-closed handling in #3 still stands.

Note on audit logging (mob-review #10)

With #6 in place, most traffic resolves to a stable userId and a real name, so per-user audit works for 1:1 and the common group case. The residual limit: when a user hasn't consented to LINE providing their info, the webhook omits userId and audit falls back to group-scope (group_id). The ADR should state this honestly rather than imply per-sender audit is always available on LINE.

is_bot is always false for LINE (line.rs:391, no bot-to-bot webhook delivery), so mob-review #13's bot-bypass semantics are a no-op for LINE — worth a one-line note.

@wangyuyan-agent wangyuyan-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the Feishu-relevant parts against the current source (gateway/src/adapters/feishu.rs, src/gateway.rs, src/config.rs). The three-layer direction (Receiver → Trust Gate → Handler) is sound, and the sender-ID table correctly flags that Feishu open_id is per-app. Three points on §7 / the Feishu specifics (details inline):

  1. should_skip_event() isn't a real symbol (the gateway-client filter is inline in run_gateway_adapter); the cited line refs also don't match the filter location in current main.
  2. The Feishu gateway-crate check is more than feishu.rs:425 — there's a sibling group allowlist (feishu.rs:443-448) with no defined destination, and for Feishu the identity is double-gated across two processes (core side fail-open when empty).
  3. is_bot lives in the Receiver's InboundEvent and drives the gate's L3 bypass, but trusted_bot_ids is slated to "stay in Handlers" — for Feishu these conflict, since is_bot can't be computed without trusted_bot_ids.

Comment thread docs/adr/identity-trust-none.md Outdated
- Echoes + drops denied events
4. **Remove scattered trust checks** — replaced by the unified Trust Gate:
- `is_denied_user()` in Discord EventHandler (`discord.rs:2892`)
- `should_skip_event()` user/channel filter in `gateway.rs` (`:832`, `:1160`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should_skip_event() isn't a symbol in the repo — grep returns zero matches. The gateway-client user/channel filter this refers to is inlined in run_gateway_adapter (src/gateway.rs), not a named function. The cited lines also look off: in current main the allowed_channels/allowed_users filter is at src/gateway.rs:785/:791, while :832/:1160 land on MessageContext construction. Since this drives a "remove these" step, the symbol/line refs should point at the real filter to stay actionable.

Comment thread docs/adr/identity-trust-none.md Outdated
- `is_denied_user()` in Discord EventHandler (`discord.rs:2892`)
- `should_skip_event()` user/channel filter in `gateway.rs` (`:832`, `:1160`)
- Inline user allowlist in Slack (`slack.rs:1224`)
- Feishu L3 check in the gateway crate (`feishu.rs:425`) — must relocate to

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two gaps for Feishu here:

  1. The gateway-crate Feishu check isn't only the user allowlist (feishu.rs:424-429). The same parse_message_event has a sibling group allowlist right after — feishu.rs:443-448 (allowed_groups, matched on chat_id). It's in neither this relocate list nor "stays in Handlers," so its destination is undefined under the "gateway = L1 only" goal.
  2. For Feishu this identity is already filtered a second time by the core gateway.rs filter two bullets up — a cross-process double gate (gateway env FEISHU_ALLOWED_USERS + core [gateway].allowed_users). They can diverge, and the core side fails open when its list is empty (resolve_allow_all = flag.unwrap_or(list.is_empty())).

- Feishu L3 check in the gateway crate (`feishu.rs:425`) — must relocate to
core, not just delete (contradicts "gateway = L1 only" model)
- Discord reaction-dispatch gating (`discord.rs:1241`)
- Note: `trusted_bot_ids`, `allow_bot_messages`, `allowed_role_ids` **stay in

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conflicts with the is_bot design for Feishu. InboundEvent.is_bot is set by the Receiver and used at the Trust Gate to bypass L3, but Feishu marks other bots as sender_type="user" (see the comment in feishu.rs), so is_bot for a non-self bot can only be derived by matching trusted_bot_ids against open_id. If trusted_bot_ids lives only in the Handler (downstream of the gate), the Receiver can't set is_bot correctly and the gate's L3 bypass is unreliable for Feishu. The gateway crate already computes is_bot from trusted_bot_ids at receive time today — so either trusted_bot_ids (or its result) must be available at the Receiver, or the ADR should carve out Feishu's is_bot derivation.

@antigenius0910

Copy link
Copy Markdown
Contributor

Slack-focused review — the general architecture is sound, but there are a few Slack-specific gaps that will bite every deployment. Line references are against crates/openab-core/src/slack.rs on main.

1. Echo delivery via DM is the wrong default for Slack

§5 says "DM-preferred, else silent drop." On Slack this breaks onboarding:

  • Many Slack apps don't request im:writeconversations.open fails → user gets silently dropped and never learns their UID.
  • chat.postEphemeral needs no extra scope beyond chat:write, posts in-channel visible only to the target user, no UID leak, no TTL problem.

Recommendation: For Slack, spec chat.postEphemeral in-channel as the primary echo path; DM is a fallback (or omitted). Otherwise the onboarding UX the ADR is trying to preserve doesn't actually work on Slack.

2. is_bot derivation must be pinned, not left as a free-floating flag

§4 says "bot messages bypass L3." That bypass is security-sensitive, but the ADR only defines is_bot as a boolean on InboundEvent. Slack's canonical rule (already used in the adapter at slack.rs:815, :864, :1206-1207) is:

is_bot = event.bot_id.is_string() || event.subtype == "bot_message"

Plus the USLACKBOT special case (slack.rs:1647 area). Please write this into the ADR — otherwise different Receiver implementations will diverge and open bypass paths.

3. Slash commands / interactive are dropped on Slack today — the ADR implies otherwise

slack.rs:797-806 explicitly ignores slash_commands and interactive envelopes (thread routing can't be reconstructed for them). But §4.2 and §7 repeatedly use "slash commands live in Handler, now gated" as a selling point and put Slack in the list. Readers will assume Slack Handler supports slash. Please either:

  • State explicitly that the Slack adapter does not consume slash_commands / interactive, or
  • Spec how a future Slack Receiver would normalize view submissions / block actions / shortcuts into InboundEvent (what maps to channel_id, is_dm, sender_id).

4. Enterprise Grid: W-prefix + team_id aren't in the trust key

"Use whichever the event payload provides" is under-specified for Grid:

  • Same person has different U/W IDs across workspaces; cross-workspace org apps may see yet a third form.
  • §5 echo rate-limit keyed on sender_id alone → a Grid user hopping workspaces bypasses it.
  • team_id is already extracted in the adapter (slack.rs:841, :1081, :1249) but only fed to streaming, not to trust.

Recommendation: For Slack, trust lookup + rate-limit should key on (team_id, sender_id), or the ADR should mandate enterprise_user.id as the canonical form and document how operators list it. Right now this is silently broken for Grid.

5. Only message flows through the Gate — reaction_added and assistant_thread_* aren't addressed

  • §5 says Slack Handler keeps emoji reactions, but doesn't say reaction_added events flow through Receiver → Gate. Grep confirms no reaction_added handling exists today, so this is aspirational. If reactions are ever wired in, reactor identity (event.user) needs to hit L3 — please state that.
  • assistant_thread_started / assistant_thread_context_changed: assistant_mode defaults to true on v0.9.0-beta.2. If these events don't flow through the Gate, an untrusted user can open an assistant thread and establish state without ever tripping L3. The ADR should specify these events go through the same pipeline with is_dm = true.

Secondary

  • §3 L1 table lists only Socket Mode for Slack. openab is Socket-only today (fine), but please state this explicitly to bound the scope — otherwise a reader assumes Events API HTTP mode with X-Slack-Signature HMAC verification is also covered, and it isn't.
  • is_dm = channel_id.starts_with('D') (slack.rs:872) misclassifies MPIMs (group DMs) in some workspace configurations — they may present as G-prefixed. If MPIMs should count as DM for allow_dm, the derivation needs to include them; if they should count as channels, please say so.

Happy to help wire any of this in.

@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

- Replace line-number refs with symbol+semantic descriptions (drift-proof)
- Rewrite echo section: platform-specific echo trait (LINE=Reply only,
  Slack=chat.postEphemeral, Discord=DM); leak-safe content by scope
- Add is_bot per-platform derivation table (pinned canonical rules)
- Document trusted_bot_ids as shared config (resolves Feishu circular dep)
- Clarify slash commands scope (Slack doesn't consume them)
- Update Slack sender ID: Enterprise Grid composite key (team_id, sender_id)
- Add non-message events section (assistant_thread_started must gate)
- Add Slack scope notes (Socket Mode only, MPIM=channel)
- Add LINE group policy: open/members dual-mode in decide()
- Add LINE @mention pre-filter as documented Receiver exception
- Feishu: gateway=L1 only, eliminate double-gating, empty list=deny-all

Addresses feedback from:
- @luffy-aiagent (LINE platform review)
- @antigenius0910 (Slack platform review)
- @wangyuyan-agent (Feishu platform review)
@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

Fixes identified during group review:

F1+F6: Add workspace_id to InboundEvent; define Slack Enterprise Grid
       canonical sender_id format and config examples for Grid deployments
F2:    Replace HashMap<String, TrustConfig> with enum PlatformTrustConfig
       (Base/Line/Slack) — LINE group policy and Slack workspace-scoped
       trust now have proper type representations
F3:    Add cron bypass in gate_event() — system-initiated events skip
       L2/L3 (platform='cron' or sender_id='openab-cron')
F4:    Add #[cfg(test)] assume_trusted_for_test() constructor for
       GatedEvent — enables Handler unit testing without full pipeline
F5+F9: Change into_inner() to pub(crate); adjust safety claim wording
       from 'bypass impossible' to 'accidental bypass compile error'
F7:    Change gate_event() signature to take InboundEvent by value —
       zero-copy hot path (no .clone() on RawPlatformEvent)
F8:    Specify bounded LRU cache (max_capacity + TTL) for rate-limit
       state — prevents OOM from random sender_id flooding
@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

chaodu-agent added 4 commits July 11, 2026 02:46
F1 (critical): Remove sender_id spoofing hole in cron bypass — only check
    platform == "cron" since WeCom allows freeform UserIDs that could
    match any synthetic value. Update rationale accordingly.

F2: Add WeCom, Google Chat, MS Teams to pinned is_bot derivation and
    echo-delivery tables (all 8 platforms now covered).

F3: Fix pseudocode precision — unwrap_or no longer borrows a temporary;
    add PlatformTrustConfigs::get() and PlatformTrustConfig::surface_allowed()
    delegating method used by gate_event.
…B11/B12/B15

Fixes:
- WeCom is_bot: remove enter_agent (user-initiated, not bot); keep only
  trusted_bot_ids (B5 F1, B12 F1)
- SlackTrustConfig::decide(): workspace_users is now strict override
  (ignore allow_all_users); base fallback supports team_id:user_id composite
  key for Enterprise Grid (B4 F1, B5 F2, B12 F3)
- LINE group policy: fix prose vs code contradiction — unconfigured groups
  use default_group_policy, not DenyScope (B12 F2)
- Reserved platform validation: MUST-level requirement for all external
  Receivers to reject reserved platform names; cron bypass invariant
  documented; Phase 1 SHOULD for typed InboundSource enum (B1)
- decide() simplified to use self.get() — remove duplicate default logic (B5 F3)
- Echo rate-limit key updated to (platform, workspace_id, sender_id) (B12 F4, B15 F2)
- Module layout: InboundEvent in mod.rs (public), gate.rs narrow (B12 F5)
- into_inner() trust boundary doc: module-level vs crate-level explained,
  Phase 1 SHOULD for lint/annotation (B11, B15 F1)
- Slack Enterprise Grid gotcha: split dense table cell into footnote (B8 F2)
InboundEvent.platform MUST be lowercase — Receivers normalize before
constructing. This ensures consistency between gate_event's == "cron"
check and PlatformTrustConfigs::get()'s to_lowercase() lookup.
Add comment noting platform field is assigned by gateway routing config,
not from webhook payload body — satisfies reserved platform invariant.
@chaodu-agent

Copy link
Copy Markdown
Collaborator Author

Note

LGTM ✅ — ADR architecture is sound, all platform reviewer feedback addressed, pseudocode matches prose commitments.

What This PR Does

Revises the identity-trust-none ADR (merged via #1264) from a single-point router gate to a three-layer adapter architecture (Receiver → Trust Gate → Handler), resolving all findings from the PR #1263 mob review and three subsequent platform-specific reviews (LINE, Slack, Feishu).

How It Works

The core insight is structural: by splitting each adapter into a Receiver (pure transport + L1 auth) and a Handler (platform-specific interaction logic), with a unified Trust Gate between them, the architecture provides a compile-time guarantee that no untrusted event can reach any Handler. This is enforced via GatedEvent (private constructor in a narrow module) — any code that tries to accept InboundEvent directly will fail to compile.

Key additions in this revision:

  • InboundEvent / GatedEvent type separation with module-level privacy
  • Per-event platform as trust lookup key (fixes unified-mode multiplexing)
  • Platform-specific extensions: LineTrustConfig (group policy open/members), SlackTrustConfig (workspace-scoped Enterprise Grid trust)
  • is_bot per-platform derivation table (all 8 platforms pinned)
  • Cron bypass via platform field only (avoids sender_id spoofing via WeCom freeform UserIDs)
  • Reserved platform validation as MUST-level requirement
  • Phased rollout (Phase 0→3) with startup errors instead of silent denial
  • Bounded LRU rate-limit cache for echo (prevents OOM flooding)
  • into_inner() pub(crate) with Phase 1 lint guidance

Findings

# Severity Finding Location
1 🟢 Type-level enforcement via GatedEvent private constructor — excellent compile-time guarantee §4.2
2 🟢 Cron bypass correctly keys on platform only (not sender_id) — prevents WeCom spoofing gate_event()
3 🟢 Platform-specific echo delivery trait separates decision (core) from mechanism (adapter) §5 echo table
4 🟢 All 3 external reviewer feedback rounds systematically addressed with clear resolution tables PR description
5 🟢 Phased rollout avoids hard cutover — operator gets explicit error, not silent breakage §6
6 🟢 Bounded rate-limit cache (LRU + TTL) prevents OOM from sender_id flooding §5 echo safeguards
What's Good (🟢)
  • Architectural soundness: The three-layer separation (Receiver → Trust Gate → Handler) is a genuine improvement over the previous single-gate-at-router design. The type-level enforcement makes accidental bypass a compile error.
  • Comprehensive platform coverage: All 8 platforms have pinned is_bot derivation rules, echo delivery mechanisms, and sender ID format documentation.
  • Addressed all reviewer feedback: LINE (6 concerns), Slack (5+2 secondary), Feishu (3 concerns) — each systematically resolved with clear rationale.
  • Security-conscious defaults: empty list = deny-all, bounded rate-limit, Reply-only for LINE echo, reserved platform rejection.
  • Implementation-ready: The pseudocode is precise enough to implement directly — gate_event() by-value ownership, run_platform generic pipeline, startup wiring, Gateway demux pattern.
  • Honest limitations documented: MPIM classification, LINE "unknown" userId, Enterprise Grid complexity — called out as known constraints rather than hidden.
Baseline Check
  • PR opened: 2026-07-04
  • Main already has: original identity-trust-none ADR (docs(adr): identity trust-none default & trust pyramid #1264, merged) with single-gate-at-router design + Phase 0/0.5 partial wiring
  • Net-new value: Complete architectural revision to three-layer model with type-level enforcement, platform-specific trust extensions (LINE group policy, Slack Enterprise Grid), all external reviewer feedback incorporated, and implementation-ready pseudocode

Addressing External Reviewer Feedback

@luffy-aiagent (LINE platform review)

6 LINE-specific concerns: echo delivery, Reply-only rule, group identity, @mention pre-filter, echo content by scope, Profile API

All addressed in d5c420c: Platform echo trait table added (LINE = Reply API only, never Push); LINE group policy open/members with "unknown" handling; @mention pre-filter documented as deliberate Receiver exception; echo content differs by scope (DM includes UID, group does not). Profile API acknowledged as P2 enhancement.

@antigenius0910 (Slack platform review)

5 main + 2 secondary concerns: echo via DM breaks onboarding, is_bot derivation pinning, slash commands scope, Enterprise Grid trust key, non-message events

All addressed in d5c420c + dcf4264 + fae8b09: Slack echo = chat.postEphemeral; is_bot per-platform derivation table pinned; slash commands scope note (Slack doesn't consume them); Enterprise Grid workspace_id + composite trust key; assistant_thread_started must flow through Trust Gate; Socket Mode scope stated; MPIM = channel documented.

@wangyuyan-agent (Feishu platform review)

3 concerns: fragile line-number refs, allowed_groups destination + double-gating + fail-open, is_bot vs trusted_bot_ids circular dependency

All addressed in d5c420c + fae8b09: Line-number refs replaced with symbol + semantic descriptions; Feishu double-gating eliminated (gateway = L1 only, empty list = deny-all); trusted_bot_ids documented as shared config readable by all layers.

5️⃣ Three Reasons We Might Not Need This PR

  1. The original ADR is already merged and Phase 0/0.5 is implemented — This revision could be a separate follow-up ADR rather than superseding the existing one, avoiding confusion about which doc is canonical.
  2. Implementation may diverge from pseudocode anyway — The ADR is now ~1100 lines of detailed pseudocode; real implementation will inevitably differ in edge cases, potentially making the ADR a maintenance burden rather than a living reference.
  3. Platform-specific extensions add complexity before validation — LINE group policy, Slack Enterprise Grid workspace-scoped trust, and the echo trait table are speculative designs that haven't been validated against real multi-platform deployments yet.

Counterarguments: (1) Superseding is cleaner — one canonical ADR, not two conflicting docs. (2) The pseudocode precision prevents the "ADR says X but code does Y" drift that plagued the original. (3) Platform reviewers validated against real source code — these aren't speculative, they're derived from actual adapter implementations.

@chaodu-agent

Copy link
Copy Markdown
Collaborator Author

Note

LGTM ✅ — All findings resolved across 4 fix commits.

What This PR Does

Revises the merged identity-trust-none ADR to a three-layer ingress architecture (Receiver → Trust Gate → Handler) with type-level enforcement (GatedEvent private constructor), platform-specific trust configs (LINE group policy, Slack Enterprise Grid), and a phased rollout plan. Docs-only: 1 file.

Review Summary

Mob review with 8 reviewers covering correctness, architecture, security, docs/UX, spec verification, and operability. All critical and important findings have been resolved in commits 3cd9cb0fae8b097a996356dc0e33.

Findings Addressed

# Severity Finding Resolution Commit
1 🔴 Cron bypass sender_id spoofing via WeCom freeform UserIDs Removed sender_id check; bypass only on platform == "cron" 3cd9cb0
2 🔴 WeCom enter_agent (user-initiated) wrongly classified as is_bot — bypasses L3 Removed; WeCom is_bot = trusted_bot_ids only fae8b09
3 🔴 Slack Grid team_id:user_id fallback format not parsed in decide() Added composite key check in base fallback fae8b09
4 🟡 allow_all_users silently overrides workspace_users workspace_users is now strict override fae8b09
5 🟡 Cron bypass relies on forgeable string — no reserved platform validation MUST-level requirement: all external Receivers reject reserved platforms; Phase 1 SHOULD use typed provenance fae8b09
6 🟡 LINE group policy prose contradicts code for unconfigured groups Prose aligned with code (default_group_policy, not DenyScope) fae8b09
7 🟡 unwrap_or borrows temporary; surface_allowed called on enum without delegating method decide() uses self.get(); PlatformTrustConfig::surface_allowed() added 3cd9cb0 + fae8b09
8 🟡 is_bot / echo-delivery tables missing WeCom, Google Chat, Teams All 8 platforms now covered 3cd9cb0
9 🟡 Echo rate-limit key missing workspace_id for Slack Grid Key = (platform, workspace_id, sender_id) fae8b09
10 🟡 InboundEvent module placement inconsistent with gate.rs scope Module layout updated: InboundEvent in mod.rs fae8b09
11 🟡 into_inner() trust boundary not documented Added trust boundary note + Phase 1 SHOULD for lint fae8b09
12 🟡 platform field case-sensitivity inconsistency between gate_event and get() Added lowercase invariant to InboundEvent.platform 7a99635
13 🟡 gw_event.platform source unclear in run_gateway_platforms Clarifying comment: assigned by gateway routing, not payload 6dc0e33
14 🟢 Type-level enforcement, ownership-transfer gate, bounded echo rate-limit, phased rollout Well-designed

What's Good

  • Three-layer architecture with compile-time trust boundary (GatedEvent private constructor)
  • Fail-closed defaults throughout (empty sender_id rejected, unknown platform = deny-all)
  • #[non_exhaustive] on Decision enum for forward compatibility
  • Feishu double-gating elimination with clear deprecation timeline
  • Comprehensive platform coverage (8 platforms, all tables pinned)
  • Phased rollout avoids hard cutover

Nits Deferred to Implementation

  • run_gateway_platforms gw_event.clone() could be a move (pseudocode only)
  • trusted_bot_ids migration path from gateway env not explicitly phased
  • ADR status still "Proposed" — update to "Accepted" on merge
  • LINE deny-echo silent drop should have warn! log for observability

@chaodu-agent

Copy link
Copy Markdown
Collaborator Author

Note

LGTM ✅ — Thorough revision of the identity-trust-none ADR, successfully addresses all platform-specific review findings with concrete, implementable design.

What This PR Does

Revises the identity-trust-none ADR from a conceptual "single router-level gate" design to a fully specified three-layer adapter architecture (Receiver → Trust Gate → Handler) with type-level enforcement (GatedEvent private constructor), addressing all findings from the mob review on PR #1263 and subsequent LINE, Slack, and Feishu platform-specific reviews.

How It Works

The ADR splits each adapter into a Receiver (transport + L1 + normalization to InboundEvent) and a Handler (platform-specific interaction logic), connected by a unified Trust Gate that produces GatedEvent — a type whose constructor is private to crate::trust::gate. This makes accidental bypass a compile error. Platform-specific extensions (LINE group policy, Slack Enterprise Grid workspace scoping) are handled via a PlatformTrustConfig enum that dispatches decide() to the appropriate variant. A phased rollout (Phase 0→3) prevents hard cutover breakage.

Findings

# Severity Finding Location
1 🟢 Type-level trust boundary design is sound — GatedEvent private constructor + module-scoped seal() gives compile-time safety guarantee §4.2
2 🟢 Comprehensive platform-specific echo delivery table addresses all reviewer concerns (LINE Reply-only, Slack ephemeral, no UID leak in groups) §5 Echo
3 🟢 Reserved platform validation with MUST-level invariants closes the cron bypass spoofing vector thoroughly §5 gate_event
4 🟢 Phased rollout (Phase 0→3) with startup-error-not-silent-denial is operator-friendly and avoids breaking deployments §6
5 🟢 Slack Enterprise Grid handling with workspace-scoped trust is well-reasoned; composite key fallback covers non-Grid tokens §5 SlackTrustConfig
6 🟢 is_bot per-platform derivation table is pinned with platform-specific gotchas documented (Feishu unreliable sender_type, WeCom enter_agent) §5 Bot handling
7 🟢 Bounded rate-limit cache requirement (LRU + TTL) addresses OOM attack vector from spoofed sender IDs §5 Echo safeguards
8 🟢 Non-message event gating table (assistant_thread_started, context_changed) closes the Slack bypass vector §7
9 🟢 New rejected alternative (gate inside Dispatcher) documents why the previous handle_message() approach was insufficient §8
Baseline Check
  • PR opened: 2026-07-04 (revised)
  • Main already has: the original identity-trust-none ADR (docs(adr): identity trust-none default & trust pyramid #1264, merged) with the single-gate AdapterRouter::handle_message() design (~355 lines)
  • Net-new value: Complete architectural redesign to three-layer Receiver/Gate/Handler split with type-level enforcement, platform-specific trust extensions (LINE groups, Slack Enterprise Grid), concrete implementation plan with phased rollout, and resolution of all platform reviewer feedback from LINE, Slack, and Feishu reviews
What's Good (🟢)
  • The GatedEvent private-constructor pattern is an excellent use of Rust's type system for security enforcement — makes "forgot to check trust" impossible at compile time
  • Cross-crate boundary documentation (gateway produces GatewayEvent, core wraps to InboundEvent) is clear about where types live
  • The into_inner() escape hatch is properly scoped (pub(crate)) with documented rationale and Phase 1 tightening recommendation
  • #[cfg(test)] assume_trusted_for_test() balances testing ergonomics with production safety
  • Each platform reviewer's concerns are individually tracked with explicit resolutions — auditable and complete
  • The "What stays in Handlers" section cleanly separates structural/trigger logic from identity trust
  • LINE @mention pre-filter documented as deliberate Receiver exception (not swept under the rug)
  • Gateway event.platform assigned by routing config (not payload body) satisfies the reserved platform invariant elegantly

5️⃣ Three Reasons We Might Not Need This PR

  1. The original ADR is already merged and Phase 0/0.5 are done on main — The implementation is progressing under the simpler model. This revision could cause confusion if implementers are already coding against the original spec. Counter: the three-layer architecture resolves fundamental design gaps (slash command bypass, Feishu double-gating) that would require rework anyway.

  2. 1160-line ADR may be too detailed for a design document — At this length, the ADR is approaching implementation-specification territory. Some details (exact is_bot derivation per platform, rate-limit cache sizing) could live in implementation docs closer to the code. Counter: the detail level is what enabled platform reviewers to validate correctness without seeing code — it's a deliberate choice for a security-critical design.

  3. Platform-specific extensions (LINE groups, Slack Grid) add complexity before proving base case — The enum dispatch (PlatformTrustConfig::Base/Line/Slack) adds indirection. Could ship the base TrustConfig first, add extensions when those platforms actually onboard. Counter: LINE and Feishu are already live on gateway — their trust quirks are known today, and designing the extension point now avoids a breaking refactor later.

@chaodu-agent

Copy link
Copy Markdown
Collaborator Author

Maintainer review — Approving

🟢 Green — solid, no action needed

  • Three-layer split fixes the real v1 flaw: the gate at handle_message() was downstream of slash commands; Receiver → Trust Gate → Handler puts it structurally upstream of all platform logic.
  • Type-level enforcement section is honest: module-level boundary, pub(crate) unwrap, cfg(test) escape hatch all documented explicitly — no overselling of compile-time safety.
  • Fail-closed everywhere it matters: empty sender_id denied, unknown platforms → deny-all default, LINE defaults to members, Feishu empty-list flipped from fail-open to deny-all, bounded LRU on the echo rate limiter.
  • Phase 2 startup error instead of silent denial — right operator experience.

🟡 Yellow — non-blocking, please address (here or in #1262)

  1. Cron bypass reintroduces the scattered-checks pattern (§7 reserved platform validation). The invariant requires every Receiver to independently reject platform == "cron" — N distributed checks where forgetting one is a trust bypass, exactly the failure mode §2 criticizes. The typed-provenance fix (InboundSource::SystemCron / External { platform }) is already in the ADR but marked SHOULD; upgrade to MUST for Phase 1 and treat the string-match blocklist as transitional only.
  2. Residual echo loop on platforms without native bot flags. On Feishu/WeCom, is_bot derives from trusted_bot_ids.contains(sender), so an unknown bot gets is_bot = false, passes L3 as a human, and triggers a deny-echo — the "bot exclusion, no echo" safeguard can't fire. Two such bots could ping-pong (bounded by the 5-min rate limit). Add one sentence to the echo-safeguards section acknowledging this.
  3. "Discord guarantees DM delivery" is inaccurate — users can disable DMs from server members, and bot DMs fail with no shared guild. Change the rationale cell to "best-effort DM."

🔴 Red — blocking

None.


Approving now; yellows are doc-level fixes that don't need a re-review.

thepagent pushed a commit that referenced this pull request Jul 11, 2026
…conformance (#1295)

* docs(platforms): add capability matrix skeleton

* docs(platforms): seed LINE notes from ADR #1291 review

* docs(platforms): docs/platforms/wecom.md (schema v1)

* docs(platforms): docs/platforms/discord.md (schema v1)

* docs(platforms): docs/platforms/_template.md (schema v1)

* docs(platforms): docs/platforms/telegram.md (schema v1)

* docs(platforms): docs/platforms/googlechat.md (schema v1)

* docs(platforms): docs/platforms/feishu.md (schema v1)

* docs(platforms): docs/platforms/slack.md (schema v1)

* docs(platforms): docs/platforms/line.md (schema v1)

* docs(platforms): docs/platforms/teams.md (schema v1)

* docs(platforms): docs/platforms/README.md (schema v1)

* docs(platforms): single date schema version, drop front-matter — docs/platforms/wecom.md

* docs(platforms): single date schema version, drop front-matter — docs/platforms/discord.md

* docs(platforms): single date schema version, drop front-matter — docs/platforms/_template.md

* docs(platforms): single date schema version, drop front-matter — docs/platforms/telegram.md

* docs(platforms): single date schema version, drop front-matter — docs/platforms/googlechat.md

* docs(platforms): single date schema version, drop front-matter — docs/platforms/feishu.md

* docs(platforms): single date schema version, drop front-matter — docs/platforms/slack.md

* docs(platforms): single date schema version, drop front-matter — docs/platforms/line.md

* docs(platforms): single date schema version, drop front-matter — docs/platforms/teams.md

* docs(platforms): single date schema version, drop front-matter — docs/platforms/README.md

* docs(platforms): add _template.toml — machine-readable schema (Option 1, serde)

Annotated TOML template that doubles as the human-readable schema reference.
Covers all three schemas from README.md (platform-capability / openab-feature-
support / platform-quirks) as typed fields + note + source, ready to be
validated by serde structs + a cargo conformance test (issue #1322).

Schema-only for now; per-platform schema/*.toml + the conformance harness
follow once the field set is confirmed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(platforms): enumerate all 16 openab_features in _template.toml

List the complete closed feature set as blank blocks (send_message …
group_routing) so the template doubles as a fill-in form, not just one
example. Clarify conformance: real schema/*.toml fully deserialize; the
template is checked for section/feature-key completeness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(platforms): source points to file (+optional #symbol), not line numbers

Line numbers go stale on any edit above the target. Switch `source` to
"file.rs" or "file.rs#symbol" — a symbol name is stable (churns only on
rename/delete, exactly the drift worth catching) and greppable, so
conformance can actually verify it rather than trivially checking a line
exists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(platforms): tighten _template.toml per review

- quirks.kind now required, named values (intrinsic | openab_decision) not A/B
- mentions: put typed `method` first for consistent fill order
- attachments: keep single headline max_size_mb, require per-type/plan
  detail in note
- Schema 1: add explicit fill guidance (note=how, source=official URL,
  "?" in note for unverified facts) so agents fill correctly

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(platforms): machine-readable schema/*.toml + conformance tests

Land the schema-driven side of the platform knowledge base (issue #1322):

- docs/platforms/schema/{line,slack,telegram,discord,feishu,wecom,
  googlechat,teams}.toml — all 8 platforms converted from their md pages
  into the typed schema (18 capability sections, the 16-feature closed
  set, quirks). Every `source` is a "file.rs" / "file.rs#symbol" code-ref,
  verified against the tree.
- crates/platform-schema — the validator + conformance tests: structural
  validation (required fields, closed enums, closed feature set, unknown-key
  rejection) + the anti-drift check that every code-ref source still exists
  (file present, #symbol greps). 3 unit + 5 conformance tests, all green.
  Uses toml_edit parse-only: no proc-macros, no build scripts, so it needs
  no C toolchain — runs on minimal CI images.
- .github/workflows/platform-schema-conformance.yml — runs the checker on
  any change to the schema files, the template, or the checker.

Excluded from the root workspace so it builds independently of the heavy
adapter crates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(platform-schema): serde structs as the authoritative schema

Switch the validator from toml_edit tree-walking back to serde-derive
structs (the agreed design in #1322): each schema/*.toml deserializes into
`Platform`, with enums as closed sets and `deny_unknown_fields` for typo
rejection. Conformance tests unchanged in intent (structural validity,
schema version, closed feature set, present-features-cite-a-source, and the
anti-drift check that every code-ref source still exists). 4 unit + 8
conformance tests, all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(platforms): address review — version, EOF newlines, PR refs, sourcing note

Resolves the CHANGES REQUESTED findings on #1295:
- F1: bump schema version 2026-07-04 → 2026-07-07 across README (current
  version + conformance table) and every page's `**Schema version:**` line,
  so it matches the TOML schema. Findings-log dates are left as-is (real
  dates, not the schema version).
- F2: add trailing newline to the platform pages that were missing one.
- F4: replace placeholder `[PR #TBD]` / `[PR: @TBD]` with `#1295`.
- F3: add a "Machine-readable schema" note to the README — the `schema/*.toml`
  files (with `file#symbol` refs) are the machine-checked source of truth;
  the `.md` `file:line` refs are a point-in-time snapshot for readability.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(platforms): drop the .md pages — schema/*.toml is the single format

The per-platform Markdown pages duplicated the schema/*.toml content (their
prose now lives in the toml `note` fields, their findings logs in `[[quirks]]`).
Remove them and _template.md; the TOML files + conformance crate are the sole
source of truth going forward.

- Delete docs/platforms/{line,slack,telegram,discord,feishu,wecom,googlechat,
  teams}.md and _template.md.
- Rewrite README as a toml-only index + schema reference (capability sections,
  the 16-feature set, quirks), pointing at schema/*.toml and _template.toml.
- Fix stale "converted from <platform>.md" provenance comments in the toml
  headers now that those md files are gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(platforms): add 4 optional capability fields

Promote known platform quirks from prose notes to typed, comparable fields
(all optional, so no forced backfill):

- attachments.max_count       — max attachments per message (Discord = 10)
- attachments.outbound_delivery — url | upload (LINE sends media by URL)
- edit_message.max_edits      — edit cap per message (Feishu = 20)
- delete_message.window_sec   — deletion window in seconds (WeCom recall = 86400)

Backfilled the platforms with known values (LINE, Discord, Feishu, WeCom);
others omit them. Template + serde structs updated; 4 unit + 8 conformance
tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(platforms): add cron_dispatch to the openab-feature closed set

#1315 landed telegram cron on main; model it in the schema. Adds
cron_dispatch as the 17th closed feature across all 8 platform pages
(discord/slack/telegram=implemented, others=not_implemented), the
template, README, and bumps schema_version to 2026-07-08.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(platforms): add missing cron_dispatch to README feature table

* docs(platforms): add workflow guide for updating features, platforms, and architecture note

* docs: add platform schema section to CONTRIBUTING.md

* docs(platforms): add architecture diagram to README

* ci(platform-schema): add --locked for reproducible conformance builds

Build strictly from the committed Cargo.lock and fail if it is stale,
rather than silently regenerating it. Partial fix for #1338 (--locked);
Rust cache, checkout@v6, and concurrency group tracked there separately.

---------

Co-authored-by: luffy-aiagent <luffy-aiagent@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
thepagent pushed a commit that referenced this pull request Jul 11, 2026
* feat(trust): Phase 1 (slack) — L3 identity via shared gate

Wire Slack into the PlatformTrustConfigs registry and call the shared
ingress trust gate from the Slack message path, mirroring the Discord
wiring from #1270. Slack was the only configured platform absent from
the registry — the gate would have fallen back to the deny-all default
had it ever run for slack events.

- main.rs: insert "slack" TrustConfig — L2 open (Slack's own channel
  allowlist stays authoritative in the adapter), L3 mirrors the
  resolved [slack].allow_all_users/allowed_users, so the gate agrees
  with Slack's existing user check (behavior-preserving).
- slack.rs: thread Arc<AdapterRouter> through run_slack_adapter into
  handle_message; evaluate gate_incoming after the existing user check
  (redundant-but-matching, non-regressive). Bots bypass L3 — same
  rationale as Discord (#1270 review F1): bot admission is
  allow_bot_messages + trusted_bot_ids, and L3 is human-identity only.
- is_dm passed truthfully via Slack conversation-ID prefix (D… = DM),
  cf. #1270 review F2; decision is identical either way today since the
  entry is L2-open with allow_dm=true.
- tests: pin the L3 bot-bypass and the DM prefix classification.

Refs #1361 (first task), umbrella #1356, ADR #1291.

* refactor(trust): consolidate l3_gate_applies into trust.rs

Self-review finding: the Slack wiring copy-pasted l3_gate_applies from
discord.rs (identical 3-line fn + doc). Move the single definition next
to Decision in trust.rs and import it from both gate call sites. Each
adapter keeps its own pinning test against the shared fn.

* docs(platforms): slack.toml reflects Phase 1 shared-gate wiring

The platform-facts KB (#1295) landed on main after this branch was cut
and records 'Slack does NOT use the shared gate' — which this PR makes
stale. Update the trust_gate feature entry (partial → implemented,
mirroring discord.toml's phrasing) and the Native-trust-divergence
quirk. Conformance suite passes against this branch's code refs.

---------

Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
thepagent pushed a commit that referenced this pull request Jul 11, 2026
Add a [line] config section for L3 identity trust, replacing the
uniform GATEWAY_ALLOW_ALL_USERS/GATEWAY_ALLOWED_USERS env vars for
LINE. First slice of #1355, mirroring the [telegram] pattern (#1297).

- config.rs: LineConfig { allow_all_users, allowed_users } with
  LINE_ALLOW_ALL_USERS / LINE_ALLOWED_USERS env fallbacks and deny-all
  default (identity-trust-none ADR). Trust-only by design — channel
  credentials stay on LINE_CHANNEL_SECRET / LINE_CHANNEL_ACCESS_TOKEN.
- main.rs: [line] (or LINE_* env) overrides the uniform GATEWAY_* seed
  in the trust registry, exactly like the telegram override. When LINE
  is active and still driven by the legacy GATEWAY_* env, log a Phase 1
  deprecation warning (becomes an error in Phase 2, #1356).
- docs: config.toml.example, config-reference.md ([line] section),
  line.md (User Trust section + env table).
- tests: TOML parse + all env resolution scenarios in one test fn
  (env-race safety, same pattern as telegram_resolve_all_scenarios).

Group policy (open/members) and Reply-API deny-echo are follow-ups on
the issue.

Refs #1355, umbrella #1356, ADR #1291.

Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
chaodu-agent added a commit that referenced this pull request Jul 11, 2026
…(Phase 1)

Clone the [line] pattern to the three remaining gateway platforms via a
shared PlatformTrustConfig (allow_all_users/allowed_users with
{PREFIX}_ALLOW_ALL_USERS / {PREFIX}_ALLOWED_USERS env fallbacks and
deny-all default). Env prefixes follow the adapters' existing
conventions: WECOM, GOOGLE_CHAT, TEAMS.

main.rs gains a platform_trust_override helper that applies the
first-class section (or its env) over the uniform GATEWAY_* seed, and
logs the shared Phase 1 deprecation warning when an active platform is
still trust-driven by the legacy env (activity signals: WECOM_CORP_ID,
GOOGLE_CHAT_ENABLED, TEAMS_APP_ID — same as has_unified_platform_env).

Trust-only by design: platform credentials stay on the gateway env
vars. Platforms needing richer trust fields later (trusted_bot_ids for
wecom/teams per their issues) graduate to their own struct, as LINE
will for group policy.

Docs: config.toml.example, config-reference.md (combined section with
per-platform sender-ID formats), wecom.md, google-chat.md,
msteams-selfhosted.md.

Refs #1358 #1359 #1360, umbrella #1356, ADR #1291. Stacked on #1365.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants