docs(adr): identity trust-none default & trust pyramid#1264
Conversation
Split out from the per-platform-config ADR. Covers the security/trust decision independently: L1/L2/L3 trust pyramid, identity trust-none default at a single router gate, TrustConfig + carrier/type changes, echo-on-deny. Depends on the first-class per-platform config ADR.
Split the trust/security decision out into a separate ADR (docs/adr/identity-trust-none.md, PR #1264). This ADR now covers only the config schema change: first-class [platform] sections + [gateway] deprecation + migration.
|
LGTM ✅ — Well-structured ADR with clear layered trust model and thorough rejected alternatives. What This PR DoesIntroduces a new ADR documenting the identity trust-none default and 3-layer trust pyramid (L1 platform auth → L2 channel scope → L3 identity trust). Flips the default from allow-all to deny-all on the identity layer, with a single un-bypassable router gate. How It WorksDefines a Findings
What's Good (🟢)
Baseline Check
|
Implementation Plan — all platforms at once (phased)This plan implements the trust pyramid for every adapter behind one shared gate, then flips the identity default to deny-all as a separate, clearly-flagged breaking change. Invariant we're enforcing
Current state (verified)
What moves to the gate vs stays in adaptersGate (router) — L2 + L3 only:
Stays in adapters (structural/trigger — NOT trust):
Rationale: adapters answer "is this message a trigger for me?"; the gate answers "is this surface/identity trusted?" Bot admission + mention modes are platform-specific trigger semantics and are intentionally out of Phase 0 — shared infra (behavior-preserving)
Phase 1 — route every platform through the gate (still behavior-preserving)
Phase 2 — echo-on-deny UX
Phase 3 — flip the default (the breaking change, isolated PR)
Why phased
Edge cases / risks
Test matrix (per platform)human-allowed / human-denied / DM-from-stranger (echo) / channel-out-of-scope (no echo) / bot-sender (bot policy, not L3) / allow_all_users=true (legacy). Deliverables
Depends on #1263 (per-platform config) for the per-platform |
* feat(trust): Phase 0 — shared TrustConfig + PlatformTrustConfigs (additive) Defines the shared L2 (scope) + L3 (identity) trust types and pure decision function per the identity-trust-none ADR (#1264). Purely additive: not yet wired into AdapterRouter, changes no runtime behavior. - TrustConfig: L2 (allow_all_channels/allowed_channels/allow_dm, default open) + L3 (allow_all_users/allowed_users, default deny-all) - Decision enum (Allow / DenyScope / DenyIdentity) — only DenyIdentity echoes (request-access UX) - PlatformTrustConfigs registry keyed by platform() (no cross-platform ID bleed) - 11 unit tests covering the L2×L3×DM decision matrix Wiring + removing scattered per-adapter checks lands in Phase 1; the trust-none default flip lands in Phase 3. Refs #1264 * docs(trust): add compact decision-flow diagram to decide() rustdoc * fix(trust): address #1266 review (F2/F3/F6/F7 + F5 doc) - F2: #[non_exhaustive] on Decision (avoid future semver break) - F3: reword DenyScope doc — scope control, not an authorization failure - F6: normalize platform keys to lowercase (case-insensitive registry) - F7: empty sender_id is never identity-allowed (fail-closed, even under allow_all_users) - F5: document new() as canonical constructor; allow_all_* takes precedence - add 3 tests (empty-sender, case-insensitive registry) --------- Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
…ared gate (#1267) * feat(trust): Phase 1 (gateway) — route gateway ingress through shared trust gate Wires the shared L2/L3 gate (#1266) into the unified gateway path: - AdapterRouter gains a PlatformTrustConfigs registry (via with_trust builder — new()'s signature unchanged) + gate_incoming() ingress gate - process_gateway_event now enforces L2/L3 via router.gate_incoming(); should_skip_event keeps only bot-filter + @mention gating (its channel/user checks are neutered in the unified path) - registry built at startup from GATEWAY_* env, keyed per gateway platform Behavior-preserving: registry defaults mirror today's should_skip_event (allow-all default); is_dm passed false so DMs are evaluated as channels exactly as today. Discord/Slack routing + dispatch-path privatization + should_skip_event L2/L3 removal follow in later PRs. Deny-flip is Phase 3. Refs #1264 * docs(trust): add phase-2 TODO for is_dm carrier at gateway gate (review F2) --------- Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
…canary before merge] (#1270) * feat(trust): Phase 1 (discord) — gate L3 identity via shared gate Routes Discord ingress through AdapterRouter::gate_incoming for the L3 (identity) layer, keyed under "discord" in the trust registry. - registry "discord" entry: L2 open + allow_dm=true (Discord's richer channel/thread/DM logic stays in the adapter — the flat allowed_channels model can't express thread-by-parent admission); L3 mirrors resolved [discord].allow_all_users/allowed_users - gate call added at the Discord dispatch spawn, redundant-but-matching with Discord's existing pre-dispatch user check → non-regressive by construction (cannot deny what already passed) Behavior-preserving. Phase 1c makes the gate authoritative and removes the scattered user check; richer Discord L2 modeling + dispatch privatization tracked in #1269. Refs #1264 #1269 * fix(trust): Discord gate skips bots + passes real is_dm (review #1270 F1/F2) F1 (blocker): Discord's is_denied_user has a !is_bot bypass (bot admission is handled by allow_bot_messages + trusted_bot_ids). The shared L3 gate is human-identity only, so running it on bots wrongly dropped trusted bot-to-bot messages when allow_all_users=false (multi-agent). Guard the gate with !sender.is_bot. F2: pass the in-scope is_dm instead of hardcoded false (benign today with the L2-open discord entry, but avoids latent risk). Note: #1267 (gateway) is unaffected — should_skip_event's user check never had a bot bypass, so the gateway gate already matched it for bots. * test(trust): name + test the Discord L3 bot-bypass (review #1270 F4) Extract the gate's bot-skip into l3_gate_applies(is_bot) and add a regression test (l3_gate_skips_bots_admits_humans) locking in that bots bypass the shared L3 gate (mirrors is_denied_user's !is_bot), so trusted/mode-admitted bots aren't denied when allow_all_users=false. --------- Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
…se 3) (#1273) * feat(trust): gateway echo-on-deny (Phase 2) + deny-all default (Phase 3) - Phase 2: on Decision::DenyIdentity, echo the sender their ID via adapter.send_message so they can request access (request-access UX). Throttled to 1 echo per (platform,sender) per 5min (LazyLock map) to prevent amplification. DenyScope stays silent (not a security boundary). - Phase 3 (gateway): flip GATEWAY_ALLOW_ALL_USERS default true→false, so gateway L3 is trust-none by default. L2 (channels) stays open. Admit via GATEWAY_ALLOWED_USERS or GATEWAY_ALLOW_ALL_USERS=true. Ships in pre-beta (self-use). emilie unaffected (own image + WS path, which doesn't use this registry/echo). Discord/Slack registry entries unchanged (still behavior-preserving allow-all). Refs #1264 #1269 * test(trust): echo throttle regression test --------- Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
* docs(adr): first-class per-platform config & trust-none default Propose promoting all gateway-connected platforms (Telegram, LINE, Feishu, WeCom, Google Chat, MS Teams) to top-level config sections, matching the existing [discord] and [slack] structure. Key decisions: - Per-platform [telegram], [line], [feishu], etc. sections - Trust-none default (empty allowed_users = deny all) - Single trust gate at AdapterRouter::handle_message() - Echo sender ID on deny - Deprecate [gateway] catch-all section Tracking: #1262 * docs(adr): add trust pyramid with platform auth comparison table Layer 1 (gateway): Platform authentication mechanisms per adapter - Telegram: secret token + IP range - LINE: HMAC-SHA256 - Feishu: SHA256 signature + encrypt key - WeCom: token signature + AES decrypt - Google Chat: JWT (RS256 via JWKS) - MS Teams: JWT (OpenID Connect) - Slack/Discord: WebSocket token auth Layer 2 (core): Channel/group trust (existing) Layer 3 (core): User trust (this ADR - flip to deny-all) * docs(adr): refine trust pyramid — L2 scope (open default) vs L3 identity (deny default) Clarify the three layers per review discussion: - L1 platform auth (security, edge) - L2 channel/group/DM scope control — NOT security, default OPEN; the platform already enforces channel membership, so L2 is operator scoping - L3 identity trust — THE security gate, default DENY-ALL, covers all paths - allow_dm is an L2 surface toggle; DMs have no platform membership gate so L3 is their sole protection - L2 must stay open by default for the echo-UID request-access flow to work * docs(adr): specify trait & type changes — extend carriers, no new trait - Extend TrustConfig with L2 scope fields (allow_all_channels, allow_dm) + surface_allowed(); defaults L2-open / L3-deny - Add 'Trait & Type Changes' section: pass SenderContext in MessageContext, add is_dm to ChannelRef, no new ChatAdapter method/trait (uniform logic) - Note the real refactor = remove scattered trust checks from discord.rs/slack.rs/gateway.rs so the router gate is un-bypassable - Fix architecture diagram gate labels (L2 optional/open, L3 deny default) * docs(adr): sharpen title to 'identity trust-none default' * docs(adr): scope down to per-platform config only Split the trust/security decision out into a separate ADR (docs/adr/identity-trust-none.md, PR #1264). This ADR now covers only the config schema change: first-class [platform] sections + [gateway] deprecation + migration. * docs(adr): mark first-class platform config as Accepted with shipped refs The design shipped across #1356/#1375 (#1297, #1365/#1366 trust slices; #1381-#1385 full config-first parity). Corrections vs implementation: googlechat field names (sa_key_json + audience), Teams sender-id is the Bot Framework activity.from.id (not AAD Object ID). The [gateway] section deprecation warning is deferred to Phase 1c (the WS two-process model still uses [gateway] legitimately) — tracked on #1356. Merging also fixes the dangling first-class-platform-config.md link in the already-merged identity-trust-none ADR. --------- Co-authored-by: 超渡法師 <chaodu-agent@openab.dev> Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
Splits the trust/security decision out of #1263 into its own ADR (per discussion — one decision per ADR).
Scope
allowed_usersfrom allow-all to deny-allAdapterRouter::handle_message()— un-bypassableMessageContext/ChannelRef, no new traitRelationship
allowed_userslive in the first-class sections.Doc:
docs/adr/identity-trust-none.md